v1-pre
This commit is contained in:
parent
572552ff13
commit
847ed92c23
10 changed files with 1232 additions and 591 deletions
|
|
@ -16,23 +16,6 @@ if (!defined('ABSPATH')) {
|
|||
*/
|
||||
class MLF_Admin_Page {
|
||||
|
||||
/**
|
||||
* Available font weights.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $weights = [
|
||||
100 => 'Thin',
|
||||
200 => 'Extra Light',
|
||||
300 => 'Light',
|
||||
400 => 'Regular',
|
||||
500 => 'Medium',
|
||||
600 => 'Semi Bold',
|
||||
700 => 'Bold',
|
||||
800 => 'Extra Bold',
|
||||
900 => 'Black',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
|
|
@ -53,7 +36,8 @@ class MLF_Admin_Page {
|
|||
$installed_fonts = $registry->get_imported_fonts();
|
||||
?>
|
||||
<div class="wrap mlf-wrap">
|
||||
<h1><?php esc_html_e('Maple Local Fonts', 'maple-local-fonts'); ?></h1>
|
||||
<h1><?php esc_html_e('Maple Fonts', 'maple-local-fonts'); ?></h1>
|
||||
<p class="mlf-description"><?php esc_html_e('Import Google Fonts to your local server for privacy-friendly, GDPR-compliant typography.', 'maple-local-fonts'); ?></p>
|
||||
|
||||
<div class="mlf-container">
|
||||
<!-- Import Section -->
|
||||
|
|
@ -61,73 +45,62 @@ class MLF_Admin_Page {
|
|||
<h2><?php esc_html_e('Import from Google Fonts', 'maple-local-fonts'); ?></h2>
|
||||
|
||||
<form id="mlf-import-form" class="mlf-form">
|
||||
<!-- Search Input -->
|
||||
<div class="mlf-form-row">
|
||||
<label for="mlf-font-name"><?php esc_html_e('Font Name', 'maple-local-fonts'); ?></label>
|
||||
<input type="text" id="mlf-font-name" name="font_name" placeholder="<?php esc_attr_e('e.g., Open Sans', 'maple-local-fonts'); ?>" required />
|
||||
<p class="description"><?php esc_html_e('Enter the exact font name as it appears on Google Fonts.', 'maple-local-fonts'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row">
|
||||
<label><?php esc_html_e('Weights', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-checkbox-grid">
|
||||
<?php foreach ($this->weights as $weight => $label) : ?>
|
||||
<label class="mlf-checkbox-label">
|
||||
<input type="checkbox" name="weights[]" value="<?php echo esc_attr($weight); ?>" <?php checked(in_array($weight, [400, 700], true)); ?> />
|
||||
<span><?php echo esc_html($weight); ?> (<?php echo esc_html($label); ?>)</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<label for="mlf-font-search"><?php esc_html_e('Search Fonts', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-search-wrapper">
|
||||
<span class="dashicons dashicons-search mlf-search-icon"></span>
|
||||
<input type="text"
|
||||
id="mlf-font-search"
|
||||
class="mlf-search-input"
|
||||
placeholder="<?php esc_attr_e('Search Google Fonts...', 'maple-local-fonts'); ?>"
|
||||
autocomplete="off" />
|
||||
<span class="spinner mlf-search-spinner" id="mlf-search-spinner"></span>
|
||||
</div>
|
||||
<p class="description"><?php esc_html_e('Type at least 2 characters to search.', 'maple-local-fonts'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row">
|
||||
<label><?php esc_html_e('Styles', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-checkbox-grid mlf-checkbox-grid-small">
|
||||
<label class="mlf-checkbox-label">
|
||||
<input type="checkbox" name="styles[]" value="normal" checked />
|
||||
<span><?php esc_html_e('Normal', 'maple-local-fonts'); ?></span>
|
||||
</label>
|
||||
<label class="mlf-checkbox-label">
|
||||
<input type="checkbox" name="styles[]" value="italic" />
|
||||
<span><?php esc_html_e('Italic', 'maple-local-fonts'); ?></span>
|
||||
<!-- Search Results -->
|
||||
<div class="mlf-search-results" id="mlf-search-results" style="display: none;">
|
||||
<div class="mlf-results-list" id="mlf-results-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Font (hidden until a font is selected) -->
|
||||
<div class="mlf-selected-font" id="mlf-selected-font" style="display: none;">
|
||||
<div class="mlf-selected-font-header">
|
||||
<span class="mlf-selected-label"><?php esc_html_e('Selected Font:', 'maple-local-fonts'); ?></span>
|
||||
<span class="mlf-selected-name" id="mlf-selected-name"></span>
|
||||
<button type="button" class="mlf-change-font" id="mlf-change-font">
|
||||
<?php esc_html_e('Change', 'maple-local-fonts'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input for font name -->
|
||||
<input type="hidden" id="mlf-font-name" name="font_name" value="" />
|
||||
|
||||
<div class="mlf-form-row mlf-italic-row">
|
||||
<label class="mlf-checkbox-label mlf-italic-toggle">
|
||||
<input type="checkbox" name="include_italic" id="mlf-include-italic" value="1" checked />
|
||||
<span><?php esc_html_e('Include Italic styles', 'maple-local-fonts'); ?></span>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e('Italic styles are useful for emphasized text. Uncheck to reduce download size.', 'maple-local-fonts'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row mlf-form-row-info">
|
||||
<span class="mlf-file-count">
|
||||
<?php esc_html_e('Files to download:', 'maple-local-fonts'); ?>
|
||||
<strong id="mlf-file-count">2</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Font Preview Section -->
|
||||
<div class="mlf-form-row mlf-preview-section" id="mlf-preview-section" style="display: none;">
|
||||
<label><?php esc_html_e('Preview', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-preview-box" id="mlf-preview-box">
|
||||
<div class="mlf-preview-text" id="mlf-preview-text">
|
||||
<span class="mlf-preview-sample mlf-preview-heading"><?php esc_html_e('The quick brown fox jumps over the lazy dog', 'maple-local-fonts'); ?></span>
|
||||
<span class="mlf-preview-sample mlf-preview-paragraph"><?php esc_html_e('ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789', 'maple-local-fonts'); ?></span>
|
||||
</div>
|
||||
<div class="mlf-preview-loading" id="mlf-preview-loading" style="display: none;">
|
||||
<span class="spinner is-active"></span>
|
||||
<span><?php esc_html_e('Loading preview...', 'maple-local-fonts'); ?></span>
|
||||
</div>
|
||||
<div class="mlf-preview-error" id="mlf-preview-error" style="display: none;">
|
||||
<?php esc_html_e('Could not load font preview. The font may not exist on Google Fonts.', 'maple-local-fonts'); ?>
|
||||
</div>
|
||||
<div class="mlf-form-row mlf-form-row-submit">
|
||||
<button type="submit" class="button button-primary" id="mlf-download-btn">
|
||||
<?php esc_html_e('Download & Install', 'maple-local-fonts'); ?>
|
||||
</button>
|
||||
<span class="spinner" id="mlf-spinner"></span>
|
||||
</div>
|
||||
<p class="description"><?php esc_html_e('Preview is loaded directly from Google Fonts. After installation, the font will be served locally.', 'maple-local-fonts'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row mlf-form-row-submit">
|
||||
<button type="submit" class="button button-primary" id="mlf-download-btn">
|
||||
<?php esc_html_e('Download & Install', 'maple-local-fonts'); ?>
|
||||
</button>
|
||||
<span class="spinner" id="mlf-spinner"></span>
|
||||
</div>
|
||||
|
||||
<div id="mlf-message" class="mlf-message" style="display: none;"></div>
|
||||
</form>
|
||||
|
||||
<div class="mlf-info-note">
|
||||
<span class="dashicons dashicons-info-outline"></span>
|
||||
<span><?php esc_html_e('All font weights (100-900) will be downloaded automatically. Variable fonts are used when available for better performance.', 'maple-local-fonts'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installed Fonts Section -->
|
||||
|
|
@ -135,7 +108,7 @@ class MLF_Admin_Page {
|
|||
<h2><?php esc_html_e('Installed Fonts', 'maple-local-fonts'); ?></h2>
|
||||
|
||||
<?php if (empty($installed_fonts)) : ?>
|
||||
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet.', 'maple-local-fonts'); ?></p>
|
||||
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet. Search and select a font above to get started.', 'maple-local-fonts'); ?></p>
|
||||
<?php else : ?>
|
||||
<div id="mlf-font-list" class="mlf-font-list">
|
||||
<?php foreach ($installed_fonts as $font) : ?>
|
||||
|
|
@ -167,20 +140,23 @@ class MLF_Admin_Page {
|
|||
<div class="mlf-section mlf-info-section">
|
||||
<?php if (wp_is_block_theme()) : ?>
|
||||
<div class="mlf-info-box">
|
||||
<span class="dashicons dashicons-info"></span>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: link to WordPress Editor */
|
||||
esc_html__('Use %s to apply fonts to your site.', 'maple-local-fonts'),
|
||||
'<a href="' . esc_url(admin_url('site-editor.php?path=%2Fwp_global_styles')) . '">' . esc_html__('Appearance → Editor → Styles → Typography', 'maple-local-fonts') . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<span class="dashicons dashicons-editor-textcolor"></span>
|
||||
<div>
|
||||
<p><strong><?php esc_html_e('How to use your fonts', 'maple-local-fonts'); ?></strong></p>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: link to WordPress Editor */
|
||||
esc_html__('Go to %s to apply fonts to your site.', 'maple-local-fonts'),
|
||||
'<a href="' . esc_url(admin_url('site-editor.php?path=%2Fwp_global_styles')) . '">' . esc_html__('Appearance → Editor → Styles → Typography', 'maple-local-fonts') . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="mlf-info-box mlf-info-box-classic">
|
||||
<span class="dashicons dashicons-info"></span>
|
||||
<span class="dashicons dashicons-editor-textcolor"></span>
|
||||
<div class="mlf-classic-theme-info">
|
||||
<p><strong><?php esc_html_e('Classic Theme Detected', 'maple-local-fonts'); ?></strong></p>
|
||||
<p><?php esc_html_e('Your theme does not support the Full Site Editor. To use imported fonts, add custom CSS to your theme:', 'maple-local-fonts'); ?></p>
|
||||
|
|
|
|||
|
|
@ -74,36 +74,13 @@ class MLF_Ajax_Handler {
|
|||
wp_send_json_error(['message' => __('Font name is too long.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Validate weights
|
||||
$weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : [];
|
||||
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
$weights = array_intersect($weights, $allowed_weights);
|
||||
|
||||
if (empty($weights)) {
|
||||
wp_send_json_error(['message' => __('At least one weight is required.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
if (count($weights) > MLF_MAX_WEIGHTS_PER_FONT) {
|
||||
wp_send_json_error(['message' => __('Too many weights selected.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Validate styles
|
||||
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
|
||||
$allowed_styles = ['normal', 'italic'];
|
||||
// Sanitize each style value before filtering
|
||||
$styles = array_map('sanitize_text_field', $styles);
|
||||
$styles = array_filter($styles, function($style) use ($allowed_styles) {
|
||||
return in_array($style, $allowed_styles, true);
|
||||
});
|
||||
|
||||
if (empty($styles)) {
|
||||
wp_send_json_error(['message' => __('At least one style is required.', 'maple-local-fonts')]);
|
||||
}
|
||||
// Validate include_italic (boolean)
|
||||
$include_italic = isset($_POST['include_italic']) && $_POST['include_italic'] === '1';
|
||||
|
||||
// 5. PROCESS REQUEST
|
||||
try {
|
||||
$downloader = new MLF_Font_Downloader();
|
||||
$download_result = $downloader->download($font_name, $weights, $styles);
|
||||
$download_result = $downloader->download($font_name, $include_italic);
|
||||
|
||||
if (is_wp_error($download_result)) {
|
||||
wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
|
||||
|
|
@ -214,12 +191,12 @@ class MLF_Ajax_Handler {
|
|||
'invalid_path' => __('Invalid file path.', 'maple-local-fonts'),
|
||||
'invalid_url' => __('Invalid font URL.', 'maple-local-fonts'),
|
||||
'invalid_name' => __('Invalid font name.', 'maple-local-fonts'),
|
||||
'invalid_weights' => __('No valid weights specified.', 'maple-local-fonts'),
|
||||
'invalid_styles' => __('No valid styles specified.', 'maple-local-fonts'),
|
||||
'not_found' => __('Font not found.', 'maple-local-fonts'),
|
||||
'not_ours' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'),
|
||||
'response_too_large' => __('The font data is too large to process. Please try selecting fewer weights.', 'maple-local-fonts'),
|
||||
'response_too_large' => __('The font data is too large to process.', 'maple-local-fonts'),
|
||||
'file_too_large' => __('The font file is too large to download.', 'maple-local-fonts'),
|
||||
'no_variable' => __('Variable font not available, trying static fonts...', 'maple-local-fonts'),
|
||||
'no_fonts' => __('No font files found. The font may not support the requested styles.', 'maple-local-fonts'),
|
||||
];
|
||||
|
||||
return $messages[$code] ?? __('An unexpected error occurred. Please try again.', 'maple-local-fonts');
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ if (!defined('ABSPATH')) {
|
|||
* Class MLF_Font_Downloader
|
||||
*
|
||||
* Handles downloading fonts from Google Fonts CSS2 API.
|
||||
* Attempts variable fonts first, falls back to static fonts.
|
||||
*/
|
||||
class MLF_Font_Downloader {
|
||||
|
||||
|
|
@ -23,38 +24,150 @@ 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.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $all_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
|
||||
/**
|
||||
* Download a font from Google Fonts.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Weights to download.
|
||||
* @param array $styles Styles to download.
|
||||
* Attempts variable font first, falls back to static if not available.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $include_italic Whether to include italic styles.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
public function download($font_name, $weights, $styles) {
|
||||
// Validate inputs
|
||||
public function download($font_name, $include_italic = true) {
|
||||
// Validate font name
|
||||
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
|
||||
return new WP_Error('invalid_name', 'Invalid font name');
|
||||
}
|
||||
|
||||
$weights = array_intersect(array_map('absint', $weights), [100, 200, 300, 400, 500, 600, 700, 800, 900]);
|
||||
if (empty($weights)) {
|
||||
return new WP_Error('invalid_weights', 'No valid weights specified');
|
||||
if (strlen($font_name) > 100) {
|
||||
return new WP_Error('invalid_name', 'Font name too long');
|
||||
}
|
||||
|
||||
$styles = array_intersect($styles, ['normal', 'italic']);
|
||||
if (empty($styles)) {
|
||||
return new WP_Error('invalid_styles', 'No valid styles specified');
|
||||
// Try variable font first
|
||||
$result = $this->try_variable_font($font_name, $include_italic);
|
||||
|
||||
if (!is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fall back to static fonts
|
||||
return $this->download_static_fonts($font_name, $include_italic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to download variable font.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $include_italic Whether to include italic styles.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
private function try_variable_font($font_name, $include_italic) {
|
||||
$font_slug = sanitize_title($font_name);
|
||||
$downloaded = [];
|
||||
|
||||
// Try to fetch variable font CSS (roman/upright)
|
||||
$css = $this->fetch_variable_css($font_name, false);
|
||||
|
||||
if (is_wp_error($css)) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
// Parse and download roman variable font
|
||||
$roman_faces = $this->parse_variable_css($css, $font_name);
|
||||
|
||||
if (is_wp_error($roman_faces) || empty($roman_faces)) {
|
||||
return new WP_Error('no_variable', 'Variable font not available');
|
||||
}
|
||||
|
||||
// Download roman variable font file(s)
|
||||
foreach ($roman_faces as $face) {
|
||||
$result = $this->download_single_file(
|
||||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
'normal',
|
||||
true // is_variable
|
||||
);
|
||||
|
||||
if (!is_wp_error($result)) {
|
||||
$downloaded[] = [
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => 'normal',
|
||||
'is_variable' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Try italic variable font if requested
|
||||
if ($include_italic) {
|
||||
$italic_css = $this->fetch_variable_css($font_name, true);
|
||||
|
||||
if (!is_wp_error($italic_css)) {
|
||||
$italic_faces = $this->parse_variable_css($italic_css, $font_name);
|
||||
|
||||
if (!is_wp_error($italic_faces) && !empty($italic_faces)) {
|
||||
foreach ($italic_faces as $face) {
|
||||
$result = $this->download_single_file(
|
||||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
'italic',
|
||||
true
|
||||
);
|
||||
|
||||
if (!is_wp_error($result)) {
|
||||
$downloaded[] = [
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => 'italic',
|
||||
'is_variable' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($downloaded)) {
|
||||
return new WP_Error('download_failed', 'Could not download variable font files');
|
||||
}
|
||||
|
||||
return [
|
||||
'font_name' => $font_name,
|
||||
'font_slug' => $font_slug,
|
||||
'files' => $downloaded,
|
||||
'is_variable' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Download static fonts (fallback when variable not available).
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $include_italic Whether to include italic styles.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
private function download_static_fonts($font_name, $include_italic) {
|
||||
$styles = $include_italic ? ['normal', 'italic'] : ['normal'];
|
||||
|
||||
// Fetch CSS from Google
|
||||
$css = $this->fetch_css($font_name, $weights, $styles);
|
||||
$css = $this->fetch_static_css($font_name, $this->all_weights, $styles);
|
||||
|
||||
if (is_wp_error($css)) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
// Parse CSS to get font face data
|
||||
$font_faces = $this->parse_css($css, $font_name);
|
||||
$font_faces = $this->parse_static_css($css, $font_name);
|
||||
|
||||
if (is_wp_error($font_faces)) {
|
||||
return $font_faces;
|
||||
}
|
||||
|
|
@ -62,42 +175,125 @@ class MLF_Font_Downloader {
|
|||
// Download each font file
|
||||
$font_slug = sanitize_title($font_name);
|
||||
$downloaded = $this->download_files($font_faces, $font_slug);
|
||||
|
||||
if (is_wp_error($downloaded)) {
|
||||
return $downloaded;
|
||||
}
|
||||
|
||||
return [
|
||||
'font_name' => $font_name,
|
||||
'font_slug' => $font_slug,
|
||||
'files' => $downloaded,
|
||||
'font_name' => $font_name,
|
||||
'font_slug' => $font_slug,
|
||||
'files' => $downloaded,
|
||||
'is_variable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Google Fonts CSS2 API URL.
|
||||
* Fetch variable font CSS from Google Fonts API.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $italic Whether to fetch italic variant.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
return $this->fetch_css($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch static font CSS from Google Fonts API.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Weights to fetch.
|
||||
* @param array $styles Styles to fetch.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_static_css($font_name, $weights, $styles) {
|
||||
$url = $this->build_static_url($font_name, $weights, $styles);
|
||||
return $this->fetch_css($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch CSS from a Google Fonts URL.
|
||||
*
|
||||
* @param string $url Google Fonts CSS URL.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_css($url) {
|
||||
// Validate URL
|
||||
if (!$this->is_valid_google_fonts_url($url)) {
|
||||
return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => MLF_REQUEST_TIMEOUT,
|
||||
'sslverify' => true,
|
||||
'user-agent' => $this->user_agent,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('request_failed', $response->get_error_message());
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status === 400) {
|
||||
return new WP_Error('font_not_found', 'Font not found');
|
||||
}
|
||||
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('http_error', 'HTTP ' . $status);
|
||||
}
|
||||
|
||||
$css = wp_remote_retrieve_body($response);
|
||||
|
||||
if (empty($css)) {
|
||||
return new WP_Error('empty_response', 'Empty response from Google Fonts');
|
||||
}
|
||||
|
||||
// Check CSS response size
|
||||
$max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024;
|
||||
if (strlen($css) > $max_size) {
|
||||
return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit');
|
||||
}
|
||||
|
||||
// Verify we got WOFF2
|
||||
if (strpos($css, '.woff2)') === false) {
|
||||
return new WP_Error('wrong_format', 'Did not receive WOFF2 format');
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build static font URL.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Array of weights.
|
||||
* @param array $styles Array of styles.
|
||||
* @return string Google Fonts CSS2 URL.
|
||||
*/
|
||||
private function build_url($font_name, $weights, $styles) {
|
||||
// URL-encode font name (spaces become +)
|
||||
private function build_static_url($font_name, $weights, $styles) {
|
||||
$family = str_replace(' ', '+', $font_name);
|
||||
|
||||
// Sort for consistent URLs
|
||||
sort($weights);
|
||||
|
||||
$has_italic = in_array('italic', $styles, true);
|
||||
$has_normal = in_array('normal', $styles, true);
|
||||
|
||||
// If only normal styles, simpler format
|
||||
if ($has_normal && !$has_italic) {
|
||||
$wght = implode(';', $weights);
|
||||
return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap";
|
||||
}
|
||||
|
||||
// Full format with ital axis
|
||||
$variations = [];
|
||||
foreach ($weights as $weight) {
|
||||
if ($has_normal) {
|
||||
|
|
@ -113,122 +309,102 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch CSS from Google Fonts API.
|
||||
* Parse variable font CSS.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Weights to fetch.
|
||||
* @param array $styles Styles to fetch.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_css($font_name, $weights, $styles) {
|
||||
$url = $this->build_url($font_name, $weights, $styles);
|
||||
|
||||
// Validate URL
|
||||
if (!$this->is_valid_google_fonts_url($url)) {
|
||||
return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
|
||||
}
|
||||
|
||||
// CRITICAL: Must use modern browser user-agent to get WOFF2
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => MLF_REQUEST_TIMEOUT,
|
||||
'sslverify' => true,
|
||||
'user-agent' => $this->user_agent,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('request_failed', $response->get_error_message());
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status === 400) {
|
||||
return new WP_Error('font_not_found', 'Font not found');
|
||||
}
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('http_error', 'HTTP ' . $status);
|
||||
}
|
||||
|
||||
$css = wp_remote_retrieve_body($response);
|
||||
if (empty($css)) {
|
||||
return new WP_Error('empty_response', 'Empty response from Google Fonts');
|
||||
}
|
||||
|
||||
// Check CSS response size to prevent memory issues
|
||||
$max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024;
|
||||
if (strlen($css) > $max_size) {
|
||||
return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit');
|
||||
}
|
||||
|
||||
// Verify we got WOFF2 (sanity check)
|
||||
if (strpos($css, '.woff2)') === false) {
|
||||
return new WP_Error('wrong_format', 'Did not receive WOFF2 format');
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Google Fonts CSS and extract font face data.
|
||||
*
|
||||
* @param string $css CSS content from Google Fonts.
|
||||
* @param string $css CSS content.
|
||||
* @param string $font_name Expected font family name.
|
||||
* @return array|WP_Error Array of font face data or error.
|
||||
*/
|
||||
private function parse_css($css, $font_name) {
|
||||
private function parse_variable_css($css, $font_name) {
|
||||
$font_faces = [];
|
||||
|
||||
// Match all @font-face blocks
|
||||
$pattern = '/@font-face\s*\{([^}]+)\}/s';
|
||||
if (!preg_match_all($pattern, $css, $matches)) {
|
||||
return new WP_Error('parse_failed', 'Could not parse CSS - no @font-face rules found');
|
||||
return new WP_Error('parse_failed', 'No @font-face rules found');
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $block) {
|
||||
$face_data = $this->parse_font_face_block($block);
|
||||
$face_data = $this->parse_font_face_block($block, true);
|
||||
|
||||
if (is_wp_error($face_data)) {
|
||||
continue; // Skip malformed blocks
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify font family matches
|
||||
if (strcasecmp($face_data['family'], $font_name) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For variable fonts, prefer latin subset
|
||||
$key = $face_data['weight'] . '-' . $face_data['style'];
|
||||
$is_latin = $this->is_latin_subset($face_data['unicode_range']);
|
||||
|
||||
if (!isset($font_faces[$key]) || $is_latin) {
|
||||
$font_faces[$key] = $face_data;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($font_faces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse static font CSS.
|
||||
*
|
||||
* @param string $css CSS content.
|
||||
* @param string $font_name Expected font family name.
|
||||
* @return array|WP_Error Array of font face data or error.
|
||||
*/
|
||||
private function parse_static_css($css, $font_name) {
|
||||
$font_faces = [];
|
||||
|
||||
$pattern = '/@font-face\s*\{([^}]+)\}/s';
|
||||
if (!preg_match_all($pattern, $css, $matches)) {
|
||||
return new WP_Error('parse_failed', 'No @font-face rules found');
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $block) {
|
||||
$face_data = $this->parse_font_face_block($block, false);
|
||||
|
||||
if (is_wp_error($face_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify font family matches (security)
|
||||
if (strcasecmp($face_data['family'], $font_name) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create unique key for weight+style combo
|
||||
$key = $face_data['weight'] . '-' . $face_data['style'];
|
||||
|
||||
// Prefer latin subset (usually comes after latin-ext)
|
||||
$is_latin = $this->is_latin_subset($face_data['unicode_range']);
|
||||
|
||||
// Only store if:
|
||||
// 1. We don't have this weight/style yet, OR
|
||||
// 2. This is latin and replaces non-latin
|
||||
if (!isset($font_faces[$key]) || $is_latin) {
|
||||
$font_faces[$key] = $face_data;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($font_faces)) {
|
||||
return new WP_Error('no_fonts', 'No valid font faces found in CSS');
|
||||
return new WP_Error('no_fonts', 'No valid font faces found');
|
||||
}
|
||||
|
||||
// Limit number of font faces to prevent excessive downloads
|
||||
// Limit number of font faces
|
||||
$max_faces = defined('MLF_MAX_FONT_FACES') ? MLF_MAX_FONT_FACES : 20;
|
||||
$font_faces_array = array_values($font_faces);
|
||||
if (count($font_faces_array) > $max_faces) {
|
||||
$font_faces_array = array_slice($font_faces_array, 0, $max_faces);
|
||||
$result = array_values($font_faces);
|
||||
|
||||
if (count($result) > $max_faces) {
|
||||
$result = array_slice($result, 0, $max_faces);
|
||||
}
|
||||
|
||||
return $font_faces_array;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single @font-face block.
|
||||
*
|
||||
* @param string $block Content inside @font-face { }.
|
||||
* @param string $block Content inside @font-face { }.
|
||||
* @param bool $is_variable Whether this is a variable font.
|
||||
* @return array|WP_Error Parsed data or error.
|
||||
*/
|
||||
private function parse_font_face_block($block) {
|
||||
private function parse_font_face_block($block, $is_variable = false) {
|
||||
$data = [];
|
||||
|
||||
// Extract font-family
|
||||
|
|
@ -238,9 +414,9 @@ class MLF_Font_Downloader {
|
|||
return new WP_Error('missing_family', 'Missing font-family');
|
||||
}
|
||||
|
||||
// Extract font-weight
|
||||
if (preg_match('/font-weight:\s*(\d+);/i', $block, $m)) {
|
||||
$data['weight'] = $m[1];
|
||||
// Extract font-weight (can be single value or range for variable)
|
||||
if (preg_match('/font-weight:\s*(\d+(?:\s+\d+)?);/i', $block, $m)) {
|
||||
$data['weight'] = trim($m[1]);
|
||||
} else {
|
||||
return new WP_Error('missing_weight', 'Missing font-weight');
|
||||
}
|
||||
|
|
@ -249,7 +425,7 @@ class MLF_Font_Downloader {
|
|||
if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) {
|
||||
$data['style'] = $m[1];
|
||||
} else {
|
||||
$data['style'] = 'normal'; // Default
|
||||
$data['style'] = 'normal';
|
||||
}
|
||||
|
||||
// Extract src URL - MUST be fonts.gstatic.com
|
||||
|
|
@ -259,7 +435,7 @@ class MLF_Font_Downloader {
|
|||
return new WP_Error('missing_src', 'Missing or invalid src URL');
|
||||
}
|
||||
|
||||
// Extract unicode-range (optional, for subset detection)
|
||||
// Extract unicode-range
|
||||
if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) {
|
||||
$data['unicode_range'] = trim($m[1]);
|
||||
} else {
|
||||
|
|
@ -277,11 +453,9 @@ class MLF_Font_Downloader {
|
|||
*/
|
||||
private function is_latin_subset($range) {
|
||||
if (empty($range)) {
|
||||
return true; // Assume latin if no range specified
|
||||
return true;
|
||||
}
|
||||
|
||||
// Latin subset typically includes basic ASCII range
|
||||
// and does NOT include extended Latin (U+0100+) as primary
|
||||
if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -305,7 +479,8 @@ class MLF_Font_Downloader {
|
|||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
$face['style']
|
||||
$face['style'],
|
||||
false
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
|
|
@ -314,13 +489,13 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
$downloaded[] = [
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => $face['style'],
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => $face['style'],
|
||||
'is_variable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// If no files downloaded, return error
|
||||
if (empty($downloaded)) {
|
||||
return new WP_Error(
|
||||
'download_failed',
|
||||
|
|
@ -332,45 +507,45 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Download a single WOFF2 file from Google Fonts.
|
||||
* Download a single WOFF2 file.
|
||||
*
|
||||
* @param string $url Google Fonts static URL.
|
||||
* @param string $font_slug Font slug for filename.
|
||||
* @param string $weight Font weight.
|
||||
* @param string $style Font style.
|
||||
* @param string $url Google Fonts static URL.
|
||||
* @param string $font_slug Font slug for filename.
|
||||
* @param string $weight Font weight (single or range).
|
||||
* @param string $style Font style.
|
||||
* @param bool $is_variable Whether this is a variable font.
|
||||
* @return string|WP_Error Local file path or error.
|
||||
*/
|
||||
private function download_single_file($url, $font_slug, $weight, $style) {
|
||||
// Validate URL is from Google
|
||||
private function download_single_file($url, $font_slug, $weight, $style, $is_variable = false) {
|
||||
if (!$this->is_valid_google_fonts_url($url)) {
|
||||
return new WP_Error('invalid_url', 'URL is not from Google Fonts');
|
||||
}
|
||||
|
||||
// Build local filename
|
||||
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight);
|
||||
// Build filename
|
||||
$weight_slug = str_replace(' ', '-', $weight);
|
||||
if ($is_variable) {
|
||||
$filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
|
||||
} else {
|
||||
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight_slug);
|
||||
}
|
||||
$filename = sanitize_file_name($filename);
|
||||
|
||||
// Validate filename
|
||||
$filename = $this->sanitize_font_filename($filename);
|
||||
if ($filename === false) {
|
||||
return new WP_Error('invalid_filename', 'Invalid filename');
|
||||
}
|
||||
|
||||
// Get destination path
|
||||
$font_dir = wp_get_font_dir();
|
||||
$destination = trailingslashit($font_dir['path']) . $filename;
|
||||
|
||||
// Validate destination path before any file operations
|
||||
if (!$this->validate_font_path($destination)) {
|
||||
return new WP_Error('invalid_path', 'Invalid destination path');
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!wp_mkdir_p($font_dir['path'])) {
|
||||
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
|
||||
}
|
||||
|
||||
// Download file
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => MLF_REQUEST_TIMEOUT,
|
||||
'sslverify' => true,
|
||||
|
|
@ -378,31 +553,29 @@ class MLF_Font_Downloader {
|
|||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('download_failed', 'Failed to download font file: ' . $response->get_error_message());
|
||||
return new WP_Error('download_failed', 'Failed to download: ' . $response->get_error_message());
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('http_error', 'Font download returned HTTP ' . $status);
|
||||
return new WP_Error('http_error', 'Download returned HTTP ' . $status);
|
||||
}
|
||||
|
||||
$content = wp_remote_retrieve_body($response);
|
||||
if (empty($content)) {
|
||||
return new WP_Error('empty_file', 'Downloaded font file is empty');
|
||||
return new WP_Error('empty_file', 'Downloaded file is empty');
|
||||
}
|
||||
|
||||
// Check font file size to prevent memory issues
|
||||
$max_size = defined('MLF_MAX_FONT_FILE_SIZE') ? MLF_MAX_FONT_FILE_SIZE : 5 * 1024 * 1024;
|
||||
if (strlen($content) > $max_size) {
|
||||
return new WP_Error('file_too_large', 'Font file exceeds maximum size limit');
|
||||
return new WP_Error('file_too_large', 'Font file exceeds maximum size');
|
||||
}
|
||||
|
||||
// Verify it looks like a WOFF2 file (magic bytes: wOF2)
|
||||
// Verify WOFF2 magic bytes
|
||||
if (substr($content, 0, 4) !== 'wOF2') {
|
||||
return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2');
|
||||
}
|
||||
|
||||
// Write file using WP Filesystem
|
||||
global $wp_filesystem;
|
||||
if (empty($wp_filesystem)) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
|
|
@ -417,10 +590,10 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate that a URL is a legitimate Google Fonts URL.
|
||||
* Validate Google Fonts URL.
|
||||
*
|
||||
* @param string $url URL to validate.
|
||||
* @return bool True if valid Google Fonts URL.
|
||||
* @return bool True if valid.
|
||||
*/
|
||||
private function is_valid_google_fonts_url($url) {
|
||||
$parsed = wp_parse_url($url);
|
||||
|
|
@ -429,7 +602,6 @@ class MLF_Font_Downloader {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Only allow Google Fonts domains
|
||||
$allowed_hosts = [
|
||||
'fonts.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
|
|
@ -439,26 +611,22 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitize and validate a font filename.
|
||||
* Sanitize font filename.
|
||||
*
|
||||
* @param string $filename The filename to validate.
|
||||
* @return string|false Sanitized filename or false if invalid.
|
||||
* @param string $filename Filename to sanitize.
|
||||
* @return string|false Sanitized filename or false.
|
||||
*/
|
||||
private function sanitize_font_filename($filename) {
|
||||
// WordPress sanitization first
|
||||
$filename = sanitize_file_name($filename);
|
||||
|
||||
// Must have .woff2 extension
|
||||
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No path components
|
||||
if ($filename !== basename($filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reasonable length
|
||||
if (strlen($filename) > 200) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -467,24 +635,21 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate that a path is within the WordPress fonts directory.
|
||||
* Validate font path is within fonts directory.
|
||||
*
|
||||
* @param string $path Full path to validate.
|
||||
* @return bool True if path is safe, false otherwise.
|
||||
* @param string $path Path to validate.
|
||||
* @return bool True if valid.
|
||||
*/
|
||||
private function validate_font_path($path) {
|
||||
$font_dir = wp_get_font_dir();
|
||||
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
|
||||
|
||||
// Resolve to real path (handles ../ etc)
|
||||
$real_path = realpath($path);
|
||||
|
||||
// If realpath fails, file doesn't exist yet - validate the directory
|
||||
if ($real_path === false) {
|
||||
$dir = dirname($path);
|
||||
$real_dir = realpath($dir);
|
||||
if ($real_dir === false) {
|
||||
// Directory doesn't exist yet, check parent
|
||||
$parent_dir = dirname($dir);
|
||||
$real_parent = realpath($parent_dir);
|
||||
if ($real_parent === false) {
|
||||
|
|
@ -498,7 +663,6 @@ class MLF_Font_Downloader {
|
|||
$real_path = wp_normalize_path($real_path);
|
||||
}
|
||||
|
||||
// Must be within fonts directory
|
||||
return strpos($real_path, $fonts_path) === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,33 +30,44 @@ class MLF_Font_Registry {
|
|||
'post_type' => 'wp_font_family',
|
||||
'name' => $font_slug,
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
'post_status' => 'any',
|
||||
]);
|
||||
|
||||
if (!empty($existing)) {
|
||||
return new WP_Error('font_exists', 'Font family already installed');
|
||||
}
|
||||
|
||||
// Get font directory
|
||||
// Get font directory info
|
||||
$font_dir = wp_get_font_dir();
|
||||
|
||||
// Build font face array for WordPress
|
||||
$font_faces = [];
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file['path']);
|
||||
$font_faces[] = [
|
||||
|
||||
// Determine if this is a variable font (weight is a range like "100 900")
|
||||
$is_variable = isset($file['is_variable']) && $file['is_variable'];
|
||||
$weight = $file['weight'];
|
||||
|
||||
$face_data = [
|
||||
'fontFamily' => $font_name,
|
||||
'fontWeight' => $file['weight'],
|
||||
'fontStyle' => $file['style'],
|
||||
'fontWeight' => $weight,
|
||||
'src' => 'file:./' . $filename,
|
||||
];
|
||||
|
||||
$font_faces[] = $face_data;
|
||||
}
|
||||
|
||||
// Build font family settings
|
||||
// Determine font category for fallback
|
||||
// Default to sans-serif, but could be enhanced to detect from Google Fonts metadata
|
||||
$fallback = 'sans-serif';
|
||||
|
||||
// Build font family settings (this is what Gutenberg reads)
|
||||
$font_family_settings = [
|
||||
'name' => $font_name,
|
||||
'slug' => $font_slug,
|
||||
'fontFamily' => sprintf('"%s", sans-serif', $font_name),
|
||||
'fontFamily' => "'{$font_name}', {$fallback}",
|
||||
'fontFace' => $font_faces,
|
||||
];
|
||||
|
||||
|
|
@ -67,7 +78,7 @@ class MLF_Font_Registry {
|
|||
'post_name' => $font_slug,
|
||||
'post_status' => 'publish',
|
||||
'post_content' => wp_json_encode($font_family_settings),
|
||||
]);
|
||||
], true);
|
||||
|
||||
if (is_wp_error($family_id)) {
|
||||
return $family_id;
|
||||
|
|
@ -77,13 +88,14 @@ class MLF_Font_Registry {
|
|||
update_post_meta($family_id, '_mlf_imported', '1');
|
||||
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
|
||||
|
||||
// Create font face posts (children)
|
||||
// Create font face posts (children) - WordPress also reads these
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file['path']);
|
||||
$weight = $file['weight'];
|
||||
|
||||
$face_settings = [
|
||||
'fontFamily' => $font_name,
|
||||
'fontWeight' => $file['weight'],
|
||||
'fontWeight' => $weight,
|
||||
'fontStyle' => $file['style'],
|
||||
'src' => 'file:./' . $filename,
|
||||
];
|
||||
|
|
@ -91,15 +103,15 @@ class MLF_Font_Registry {
|
|||
wp_insert_post([
|
||||
'post_type' => 'wp_font_face',
|
||||
'post_parent' => $family_id,
|
||||
'post_title' => sprintf('%s %s %s', $font_name, $file['weight'], $file['style']),
|
||||
'post_title' => sprintf('%s %s %s', $font_name, $weight, $file['style']),
|
||||
'post_name' => sanitize_title(sprintf('%s-%s-%s', $font_slug, $weight, $file['style'])),
|
||||
'post_status' => 'publish',
|
||||
'post_content' => wp_json_encode($face_settings),
|
||||
]);
|
||||
}
|
||||
|
||||
// Clear font caches
|
||||
delete_transient('wp_font_library_fonts');
|
||||
delete_transient('mlf_imported_fonts_list');
|
||||
// Clear all font-related caches
|
||||
$this->clear_font_caches();
|
||||
|
||||
return $family_id;
|
||||
}
|
||||
|
|
@ -154,13 +166,40 @@ class MLF_Font_Registry {
|
|||
// Delete family post
|
||||
wp_delete_post($family_id, true);
|
||||
|
||||
// Clear caches
|
||||
delete_transient('wp_font_library_fonts');
|
||||
delete_transient('mlf_imported_fonts_list');
|
||||
// Clear all font-related caches
|
||||
$this->clear_font_caches();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all font-related caches.
|
||||
*/
|
||||
private function clear_font_caches() {
|
||||
// Clear WordPress Font Library cache
|
||||
delete_transient('wp_font_library_fonts');
|
||||
|
||||
// Clear our plugin's cache
|
||||
delete_transient('mlf_imported_fonts_list');
|
||||
|
||||
// Clear global settings cache (used by Gutenberg)
|
||||
wp_cache_delete('wp_get_global_settings', 'theme_json');
|
||||
wp_cache_delete('wp_get_global_stylesheet', 'theme_json');
|
||||
|
||||
// Clear theme.json related caches
|
||||
delete_transient('global_styles');
|
||||
delete_transient('global_styles_' . get_stylesheet());
|
||||
|
||||
// Clear object cache for post queries
|
||||
wp_cache_flush_group('posts');
|
||||
|
||||
// Clear any theme mods cache
|
||||
delete_transient('theme_mods_' . get_stylesheet());
|
||||
|
||||
// Trigger action for other plugins/themes that might cache fonts
|
||||
do_action('mlf_fonts_cache_cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fonts imported by this plugin.
|
||||
*
|
||||
|
|
@ -193,9 +232,9 @@ class MLF_Font_Registry {
|
|||
|
||||
// Single query to get ALL font faces for ALL fonts (fixes N+1)
|
||||
$all_faces = get_posts([
|
||||
'post_type' => 'wp_font_face',
|
||||
'posts_per_page' => 1000, // Max 100 fonts × 9 weights × 2 styles = 1800, but limit reasonably
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'wp_font_face',
|
||||
'posts_per_page' => 1000,
|
||||
'post_status' => 'publish',
|
||||
'post_parent__in' => $font_ids,
|
||||
]);
|
||||
|
||||
|
|
@ -234,7 +273,11 @@ class MLF_Font_Registry {
|
|||
|
||||
// Sort variants by weight then style
|
||||
usort($variants, function($a, $b) {
|
||||
$weight_cmp = intval($a['weight']) - intval($b['weight']);
|
||||
// Handle weight ranges (variable fonts)
|
||||
$a_weight = is_numeric($a['weight']) ? intval($a['weight']) : intval(explode(' ', $a['weight'])[0]);
|
||||
$b_weight = is_numeric($b['weight']) ? intval($b['weight']) : intval(explode(' ', $b['weight'])[0]);
|
||||
|
||||
$weight_cmp = $a_weight - $b_weight;
|
||||
if ($weight_cmp !== 0) {
|
||||
return $weight_cmp;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
<?php
|
||||
/**
|
||||
* Font Search for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Font_Search
|
||||
*
|
||||
* Handles searching Google Fonts metadata.
|
||||
*/
|
||||
class MLF_Font_Search {
|
||||
|
||||
/**
|
||||
* Cache key for fonts metadata.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_KEY = 'mlf_google_fonts_metadata';
|
||||
|
||||
/**
|
||||
* Cache duration in seconds (24 hours).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_DURATION = DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Google Fonts metadata URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_URL = 'https://fonts.google.com/metadata/fonts';
|
||||
|
||||
/**
|
||||
* Maximum metadata response size (2MB).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_METADATA_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Rate limiter instance.
|
||||
*
|
||||
* @var MLF_Rate_Limiter
|
||||
*/
|
||||
private $rate_limiter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_mlf_search_fonts', [$this, 'handle_search']);
|
||||
// No nopriv - admin only
|
||||
|
||||
// Initialize rate limiter: 30 requests per minute (search can be rapid while typing)
|
||||
$this->rate_limiter = new MLF_Rate_Limiter(30, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle font search AJAX request.
|
||||
*/
|
||||
public function handle_search() {
|
||||
// 1. NONCE CHECK
|
||||
if (!check_ajax_referer('mlf_search_fonts', '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('search')) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 4. INPUT VALIDATION
|
||||
$query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : '';
|
||||
|
||||
if (strlen($query) < 2) {
|
||||
wp_send_json_success(['fonts' => []]);
|
||||
}
|
||||
|
||||
// Limit query length
|
||||
if (strlen($query) > 100) {
|
||||
wp_send_json_error(['message' => __('Search query too long.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// 5. PROCESS REQUEST
|
||||
$fonts = $this->get_fonts_metadata();
|
||||
|
||||
if (is_wp_error($fonts)) {
|
||||
wp_send_json_error(['message' => $fonts->get_error_message()]);
|
||||
}
|
||||
|
||||
// Search fonts
|
||||
$results = $this->search_fonts($fonts, $query);
|
||||
|
||||
wp_send_json_success(['fonts' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google Fonts metadata (cached).
|
||||
*
|
||||
* @return array|WP_Error Array of fonts or error.
|
||||
*/
|
||||
public function get_fonts_metadata() {
|
||||
// Check cache first
|
||||
$cached = get_transient(self::CACHE_KEY);
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// Fetch from Google
|
||||
$response = wp_remote_get(self::METADATA_URL, [
|
||||
'timeout' => 30,
|
||||
'sslverify' => true,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('fetch_failed', __('Could not fetch fonts list from Google.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('fetch_failed', __('Google Fonts returned an error.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
// Validate response size
|
||||
if (strlen($body) > self::MAX_METADATA_SIZE) {
|
||||
return new WP_Error('response_too_large', __('Font metadata response too large.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
// Validate response is not empty
|
||||
if (empty($body)) {
|
||||
return new WP_Error('empty_response', __('Empty response from Google Fonts.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
// Google's metadata has a )]}' prefix for security, remove it
|
||||
$body = preg_replace('/^\)\]\}\'\s*/', '', $body);
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return new WP_Error('parse_failed', __('Could not parse fonts data.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
if (!$data || !isset($data['familyMetadataList'])) {
|
||||
return new WP_Error('parse_failed', __('Could not parse fonts data.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
// Process and simplify the data
|
||||
$fonts = [];
|
||||
foreach ($data['familyMetadataList'] as $font) {
|
||||
$fonts[] = [
|
||||
'family' => $font['family'],
|
||||
'category' => $font['category'] ?? 'sans-serif',
|
||||
'variants' => $font['fonts'] ?? [],
|
||||
'subsets' => $font['subsets'] ?? ['latin'],
|
||||
];
|
||||
}
|
||||
|
||||
// Cache for 24 hours
|
||||
set_transient(self::CACHE_KEY, $fonts, self::CACHE_DURATION);
|
||||
|
||||
return $fonts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search fonts by query.
|
||||
*
|
||||
* @param array $fonts All fonts.
|
||||
* @param string $query Search query.
|
||||
* @return array Matching fonts (max 20).
|
||||
*/
|
||||
private function search_fonts($fonts, $query) {
|
||||
$query_lower = strtolower($query);
|
||||
$exact_matches = [];
|
||||
$starts_with = [];
|
||||
$contains = [];
|
||||
|
||||
foreach ($fonts as $font) {
|
||||
$family_lower = strtolower($font['family']);
|
||||
|
||||
// Exact match
|
||||
if ($family_lower === $query_lower) {
|
||||
$exact_matches[] = $this->format_font_result($font);
|
||||
}
|
||||
// Starts with query
|
||||
elseif (strpos($family_lower, $query_lower) === 0) {
|
||||
$starts_with[] = $this->format_font_result($font);
|
||||
}
|
||||
// Contains query
|
||||
elseif (strpos($family_lower, $query_lower) !== false) {
|
||||
$contains[] = $this->format_font_result($font);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine results: exact matches first, then starts with, then contains
|
||||
$results = array_merge($exact_matches, $starts_with, $contains);
|
||||
|
||||
// Limit to 20 results
|
||||
return array_slice($results, 0, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a font for the search results.
|
||||
*
|
||||
* @param array $font Font data.
|
||||
* @return array Formatted font.
|
||||
*/
|
||||
private function format_font_result($font) {
|
||||
// Check if font has variable version
|
||||
$has_variable = false;
|
||||
$has_italic = false;
|
||||
$weights = [];
|
||||
|
||||
if (!empty($font['variants'])) {
|
||||
foreach ($font['variants'] as $variant => $data) {
|
||||
// Variable fonts have ranges like "100..900"
|
||||
if (strpos($variant, '..') !== false) {
|
||||
$has_variable = true;
|
||||
}
|
||||
if (strpos($variant, 'i') !== false || strpos($variant, 'italic') !== false) {
|
||||
$has_italic = true;
|
||||
}
|
||||
// Extract weight
|
||||
$weight = preg_replace('/[^0-9]/', '', $variant);
|
||||
if ($weight && !in_array($weight, $weights, true)) {
|
||||
$weights[] = $weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort weights
|
||||
sort($weights, SORT_NUMERIC);
|
||||
|
||||
return [
|
||||
'family' => $font['family'],
|
||||
'category' => $font['category'],
|
||||
'has_variable' => $has_variable,
|
||||
'has_italic' => $has_italic,
|
||||
'weights' => $weights,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the fonts metadata cache.
|
||||
*/
|
||||
public function clear_cache() {
|
||||
delete_transient(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
|
|
@ -258,8 +258,7 @@ class MLF_Rest_Controller extends WP_REST_Controller {
|
|||
*/
|
||||
public function create_item($request) {
|
||||
$font_name = sanitize_text_field($request->get_param('font_name'));
|
||||
$weights = array_map('absint', (array) $request->get_param('weights'));
|
||||
$styles = array_map('sanitize_text_field', (array) $request->get_param('styles'));
|
||||
$include_italic = (bool) $request->get_param('include_italic');
|
||||
|
||||
// Validate font name
|
||||
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
|
||||
|
|
@ -278,43 +277,9 @@ class MLF_Rest_Controller extends WP_REST_Controller {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate weights
|
||||
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
$weights = array_intersect($weights, $allowed_weights);
|
||||
|
||||
if (empty($weights)) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('At least one valid weight is required.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
if (count($weights) > MLF_MAX_WEIGHTS_PER_FONT) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('Too many weights selected.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Validate styles
|
||||
$allowed_styles = ['normal', 'italic'];
|
||||
$styles = array_filter($styles, function($style) use ($allowed_styles) {
|
||||
return in_array($style, $allowed_styles, true);
|
||||
});
|
||||
|
||||
if (empty($styles)) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('At least one valid style is required.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$downloader = new MLF_Font_Downloader();
|
||||
$download_result = $downloader->download($font_name, $weights, $styles);
|
||||
$download_result = $downloader->download($font_name, $include_italic);
|
||||
|
||||
if (is_wp_error($download_result)) {
|
||||
return new WP_Error(
|
||||
|
|
@ -455,23 +420,10 @@ class MLF_Rest_Controller extends WP_REST_Controller {
|
|||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
'weights' => [
|
||||
'description' => __('Array of font weights to download.', 'maple-local-fonts'),
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'integer',
|
||||
'enum' => [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
],
|
||||
],
|
||||
'styles' => [
|
||||
'description' => __('Array of font styles to download.', 'maple-local-fonts'),
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['normal', 'italic'],
|
||||
],
|
||||
'include_italic' => [
|
||||
'description' => __('Whether to include italic styles.', 'maple-local-fonts'),
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue