font handling fix
This commit is contained in:
parent
d5ecb31dad
commit
fda7bd1d12
6 changed files with 727 additions and 22 deletions
|
|
@ -520,6 +520,33 @@
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings Toggle */
|
||||||
|
.mlf-setting-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdcde;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: normal !important;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mlf-setting-toggle:hover {
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mlf-setting-toggle input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mlf-settings-section .description {
|
||||||
|
margin-top: 8px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading State */
|
/* Loading State */
|
||||||
.mlf-loading {
|
.mlf-loading {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,39 @@ class MLF_Admin_Page {
|
||||||
// Empty constructor - class is instantiated for rendering
|
// Empty constructor - class is instantiated for rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle settings form submission.
|
||||||
|
*/
|
||||||
|
private function handle_settings_save() {
|
||||||
|
if (!isset($_POST['mlf_save_settings'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['mlf_settings_nonce']) || !wp_verify_nonce($_POST['mlf_settings_nonce'], 'mlf_save_settings')) {
|
||||||
|
add_settings_error('mlf_settings', 'nonce_error', __('Security check failed.', 'maple-local-fonts'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify capability
|
||||||
|
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
|
||||||
|
if (!current_user_can($capability)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save compatibility mode setting
|
||||||
|
$compatibility_mode = isset($_POST['mlf_compatibility_mode']) && $_POST['mlf_compatibility_mode'] === '1';
|
||||||
|
update_option('mlf_compatibility_mode', $compatibility_mode);
|
||||||
|
|
||||||
|
// Clear font caches to apply new URL format
|
||||||
|
delete_transient('mlf_imported_fonts_list');
|
||||||
|
if (class_exists('WP_Theme_JSON_Resolver')) {
|
||||||
|
WP_Theme_JSON_Resolver::clean_cached_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
add_settings_error('mlf_settings', 'settings_saved', __('Settings saved.', 'maple-local-fonts'), 'success');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the admin page.
|
* Render the admin page.
|
||||||
*/
|
*/
|
||||||
|
|
@ -32,11 +65,15 @@ class MLF_Admin_Page {
|
||||||
wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'maple-local-fonts'));
|
wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'maple-local-fonts'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle settings save
|
||||||
|
$this->handle_settings_save();
|
||||||
|
|
||||||
$registry = new MLF_Font_Registry();
|
$registry = new MLF_Font_Registry();
|
||||||
$installed_fonts = $registry->get_imported_fonts();
|
$installed_fonts = $registry->get_imported_fonts();
|
||||||
?>
|
?>
|
||||||
<div class="wrap mlf-wrap">
|
<div class="wrap mlf-wrap">
|
||||||
<h1><?php esc_html_e('Maple Fonts', 'maple-local-fonts'); ?></h1>
|
<h1><?php esc_html_e('Maple Fonts', 'maple-local-fonts'); ?></h1>
|
||||||
|
<?php settings_errors('mlf_settings'); ?>
|
||||||
<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>
|
<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">
|
<div class="mlf-container">
|
||||||
|
|
@ -162,6 +199,28 @@ class MLF_Admin_Page {
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<div class="mlf-section mlf-settings-section">
|
||||||
|
<h2><?php esc_html_e('Settings', 'maple-local-fonts'); ?></h2>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field('mlf_save_settings', 'mlf_settings_nonce'); ?>
|
||||||
|
<div class="mlf-form-row">
|
||||||
|
<label class="mlf-checkbox-label mlf-setting-toggle">
|
||||||
|
<input type="checkbox" name="mlf_compatibility_mode" value="1" <?php checked(get_option('mlf_compatibility_mode', true)); ?> />
|
||||||
|
<span><strong><?php esc_html_e('Compatibility Mode', 'maple-local-fonts'); ?></strong></span>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e('Enable this if fonts are not loading on mobile devices or behind reverse proxies (Caddy, Nginx, Cloudflare). This serves fonts through WordPress for guaranteed header compatibility.', 'maple-local-fonts'); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mlf-form-row mlf-form-row-submit">
|
||||||
|
<button type="submit" name="mlf_save_settings" class="button button-secondary">
|
||||||
|
<?php esc_html_e('Save Settings', 'maple-local-fonts'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Info Section -->
|
<!-- Info Section -->
|
||||||
<div class="mlf-section mlf-info-section">
|
<div class="mlf-section mlf-info-section">
|
||||||
<?php if (wp_is_block_theme()) : ?>
|
<?php if (wp_is_block_theme()) : ?>
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ class MLF_Font_Downloader {
|
||||||
[300, 400, 500, 600, 700], // Common range
|
[300, 400, 500, 600, 700], // Common range
|
||||||
[400, 500, 600, 700], // Without light
|
[400, 500, 600, 700], // Without light
|
||||||
[400, 700], // Just regular and bold
|
[400, 700], // Just regular and bold
|
||||||
|
[400], // Single weight (display fonts)
|
||||||
];
|
];
|
||||||
|
|
||||||
$css = null;
|
$css = null;
|
||||||
|
|
@ -227,7 +228,8 @@ class MLF_Font_Downloader {
|
||||||
$family = str_replace(' ', '+', $font_name);
|
$family = str_replace(' ', '+', $font_name);
|
||||||
|
|
||||||
// Try different weight ranges - fonts support varying ranges
|
// Try different weight ranges - fonts support varying ranges
|
||||||
$weight_ranges = ['300..900', '300..800', '300..700', '400..700'];
|
// Also try single weight 400 for fonts that only have one weight
|
||||||
|
$weight_ranges = ['300..900', '300..800', '300..700', '400..700', '400'];
|
||||||
|
|
||||||
foreach ($weight_ranges as $range) {
|
foreach ($weight_ranges as $range) {
|
||||||
if ($italic) {
|
if ($italic) {
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,10 @@ class MLF_Font_Registry {
|
||||||
return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
|
return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get font info before deletion for cleanup
|
||||||
|
$settings = json_decode($family->post_content, true);
|
||||||
|
$font_slug = $settings['slug'] ?? $family->post_name;
|
||||||
|
|
||||||
// Get font faces (children)
|
// Get font faces (children)
|
||||||
$faces = get_children([
|
$faces = get_children([
|
||||||
'post_parent' => $family_id,
|
'post_parent' => $family_id,
|
||||||
|
|
@ -154,11 +158,11 @@ class MLF_Font_Registry {
|
||||||
|
|
||||||
// Delete font face files and posts
|
// Delete font face files and posts
|
||||||
foreach ($faces as $face) {
|
foreach ($faces as $face) {
|
||||||
$settings = json_decode($face->post_content, true);
|
$face_settings = json_decode($face->post_content, true);
|
||||||
|
|
||||||
if (isset($settings['src'])) {
|
if (isset($face_settings['src'])) {
|
||||||
// Convert file:. URL to path
|
// Convert file:. URL to path
|
||||||
$src = $settings['src'];
|
$src = $face_settings['src'];
|
||||||
$src = str_replace('file:./', '', $src);
|
$src = str_replace('file:./', '', $src);
|
||||||
$file_path = trailingslashit($font_dir['path']) . basename($src);
|
$file_path = trailingslashit($font_dir['path']) . basename($src);
|
||||||
|
|
||||||
|
|
@ -176,12 +180,83 @@ class MLF_Font_Registry {
|
||||||
// Delete family post
|
// Delete family post
|
||||||
wp_delete_post($family_id, true);
|
wp_delete_post($family_id, true);
|
||||||
|
|
||||||
|
// Clean up global styles references to this font
|
||||||
|
$this->remove_font_from_global_styles($font_slug);
|
||||||
|
|
||||||
// Clear all font-related caches
|
// Clear all font-related caches
|
||||||
$this->clear_font_caches();
|
$this->clear_font_caches();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove references to a deleted font from global styles.
|
||||||
|
*
|
||||||
|
* This prevents block errors when a font is deleted but still referenced.
|
||||||
|
*
|
||||||
|
* @param string $font_slug The font slug to remove.
|
||||||
|
*/
|
||||||
|
private function remove_font_from_global_styles($font_slug) {
|
||||||
|
// Get the global styles post for the current theme
|
||||||
|
$global_styles = get_posts([
|
||||||
|
'post_type' => 'wp_global_styles',
|
||||||
|
'posts_per_page' => 1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'tax_query' => [
|
||||||
|
[
|
||||||
|
'taxonomy' => 'wp_theme',
|
||||||
|
'field' => 'name',
|
||||||
|
'terms' => get_stylesheet(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($global_styles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$global_styles_post = $global_styles[0];
|
||||||
|
$content = json_decode($global_styles_post->post_content, true);
|
||||||
|
|
||||||
|
if (empty($content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modified = false;
|
||||||
|
|
||||||
|
// Helper function to recursively remove font references
|
||||||
|
$remove_font_refs = function (&$data) use ($font_slug, &$modified, &$remove_font_refs) {
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($data as $key => &$value) {
|
||||||
|
// Check for fontFamily references using the slug pattern
|
||||||
|
if ($key === 'fontFamily' && is_string($value)) {
|
||||||
|
// WordPress uses format like: var(--wp--preset--font-family--font-slug)
|
||||||
|
if (strpos($value, '--font-family--' . $font_slug) !== false) {
|
||||||
|
unset($data[$key]);
|
||||||
|
$modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for typography.fontFamily in element/block styles
|
||||||
|
if (is_array($value)) {
|
||||||
|
$remove_font_refs($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$remove_font_refs($content);
|
||||||
|
|
||||||
|
if ($modified) {
|
||||||
|
wp_update_post([
|
||||||
|
'ID' => $global_styles_post->ID,
|
||||||
|
'post_content' => wp_json_encode($content),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all font-related caches.
|
* Clear all font-related caches.
|
||||||
*/
|
*/
|
||||||
|
|
@ -320,6 +395,82 @@ class MLF_Font_Registry {
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all fonts imported by this plugin, including actual filenames.
|
||||||
|
*
|
||||||
|
* This method includes the actual filename stored in the database,
|
||||||
|
* which is needed to correctly reference variable vs static font files.
|
||||||
|
*
|
||||||
|
* @return array Array of font data with filenames.
|
||||||
|
*/
|
||||||
|
public function get_imported_fonts_with_src() {
|
||||||
|
$fonts = get_posts([
|
||||||
|
'post_type' => 'wp_font_family',
|
||||||
|
'posts_per_page' => 100,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'meta_key' => '_mlf_imported',
|
||||||
|
'meta_value' => '1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($fonts)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all font IDs for batch query
|
||||||
|
$font_ids = wp_list_pluck($fonts, 'ID');
|
||||||
|
|
||||||
|
// Single query to get ALL font faces for ALL fonts
|
||||||
|
$all_faces = get_posts([
|
||||||
|
'post_type' => 'wp_font_face',
|
||||||
|
'posts_per_page' => 1000,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_parent__in' => $font_ids,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Group faces by parent font ID
|
||||||
|
$faces_by_font = [];
|
||||||
|
foreach ($all_faces as $face) {
|
||||||
|
$parent_id = $face->post_parent;
|
||||||
|
if (!isset($faces_by_font[$parent_id])) {
|
||||||
|
$faces_by_font[$parent_id] = [];
|
||||||
|
}
|
||||||
|
$faces_by_font[$parent_id][] = $face;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($fonts as $font) {
|
||||||
|
$settings = json_decode($font->post_content, true);
|
||||||
|
|
||||||
|
// Get variants from pre-fetched data
|
||||||
|
$faces = $faces_by_font[$font->ID] ?? [];
|
||||||
|
|
||||||
|
$variants = [];
|
||||||
|
foreach ($faces as $face) {
|
||||||
|
$face_settings = json_decode($face->post_content, true);
|
||||||
|
|
||||||
|
// Extract filename from src (format: "file:./filename.woff2")
|
||||||
|
$src = $face_settings['src'] ?? '';
|
||||||
|
$filename = str_replace('file:./', '', $src);
|
||||||
|
|
||||||
|
$variants[] = [
|
||||||
|
'weight' => $face_settings['fontWeight'] ?? '400',
|
||||||
|
'style' => $face_settings['fontStyle'] ?? 'normal',
|
||||||
|
'filename' => $filename,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'id' => $font->ID,
|
||||||
|
'name' => $settings['name'] ?? $font->post_title,
|
||||||
|
'slug' => $settings['slug'] ?? $font->post_name,
|
||||||
|
'variants' => $variants,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that a path is within the WordPress fonts directory.
|
* Validate that a path is within the WordPress fonts directory.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Font Server for Maple Local Fonts.
|
||||||
|
*
|
||||||
|
* Serves font files and CSS through PHP with proper headers.
|
||||||
|
* This ensures CORS and MIME types work regardless of server configuration.
|
||||||
|
*
|
||||||
|
* @package Maple_Local_Fonts
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MLF_Font_Server
|
||||||
|
*
|
||||||
|
* Handles serving font files and CSS with proper headers via REST API.
|
||||||
|
*/
|
||||||
|
class MLF_Font_Server {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API routes.
|
||||||
|
*/
|
||||||
|
public function register_routes() {
|
||||||
|
// Serve individual font files
|
||||||
|
register_rest_route('mlf/v1', '/font/(?P<filename>[a-zA-Z0-9_\-]+\.woff2)', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [$this, 'serve_font'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
'args' => [
|
||||||
|
'filename' => [
|
||||||
|
'required' => true,
|
||||||
|
'validate_callback' => [$this, 'validate_filename'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Serve CSS file (like Google Fonts does)
|
||||||
|
register_rest_route('mlf/v1', '/css', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [$this, 'serve_css'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate font filename.
|
||||||
|
*
|
||||||
|
* @param string $filename The filename to validate.
|
||||||
|
* @return bool True if valid.
|
||||||
|
*/
|
||||||
|
public function validate_filename($filename) {
|
||||||
|
// Only allow alphanumeric, underscore, hyphen, and .woff2 extension
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9_\-]+\.woff2$/', $filename)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent directory traversal
|
||||||
|
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve a font file with proper headers.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request The request object.
|
||||||
|
* @return WP_REST_Response|WP_Error Response or error.
|
||||||
|
*/
|
||||||
|
public function serve_font($request) {
|
||||||
|
$filename = $request->get_param('filename');
|
||||||
|
|
||||||
|
// Get font directory
|
||||||
|
$font_dir = wp_get_font_dir();
|
||||||
|
$file_path = trailingslashit($font_dir['path']) . $filename;
|
||||||
|
|
||||||
|
// Validate file exists and is within fonts directory
|
||||||
|
$real_path = realpath($file_path);
|
||||||
|
$fonts_path = realpath($font_dir['path']);
|
||||||
|
|
||||||
|
if (!$real_path || !$fonts_path || strpos($real_path, $fonts_path) !== 0) {
|
||||||
|
return new WP_Error('not_found', 'Font not found', ['status' => 404]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($real_path)) {
|
||||||
|
return new WP_Error('not_found', 'Font not found', ['status' => 404]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's a woff2 file
|
||||||
|
if (pathinfo($real_path, PATHINFO_EXTENSION) !== 'woff2') {
|
||||||
|
return new WP_Error('invalid_type', 'Invalid file type', ['status' => 400]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
$content = file_get_contents($real_path);
|
||||||
|
|
||||||
|
if ($content === false) {
|
||||||
|
return new WP_Error('read_error', 'Could not read font file', ['status' => 500]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify WOFF2 magic bytes
|
||||||
|
if (substr($content, 0, 4) !== 'wOF2') {
|
||||||
|
return new WP_Error('invalid_format', 'Invalid font format', ['status' => 400]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send font with proper headers
|
||||||
|
$response = new WP_REST_Response($content);
|
||||||
|
$response->set_status(200);
|
||||||
|
$response->set_headers([
|
||||||
|
'Content-Type' => 'font/woff2',
|
||||||
|
'Content-Length' => strlen($content),
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
|
||||||
|
'Cross-Origin-Resource-Policy' => 'cross-origin',
|
||||||
|
'Cache-Control' => 'public, max-age=31536000, immutable',
|
||||||
|
'Vary' => 'Accept-Encoding',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve CSS file with @font-face rules (mimics Google Fonts).
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request The request object.
|
||||||
|
* @return WP_REST_Response Response with CSS content.
|
||||||
|
*/
|
||||||
|
public function serve_css($request) {
|
||||||
|
$registry = new MLF_Font_Registry();
|
||||||
|
$fonts = $registry->get_imported_fonts();
|
||||||
|
|
||||||
|
if (empty($fonts)) {
|
||||||
|
$response = new WP_REST_Response('/* No fonts installed */');
|
||||||
|
$response->set_headers(['Content-Type' => 'text/css; charset=UTF-8']);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$css = "/* Maple Local Fonts - Generated CSS */\n\n";
|
||||||
|
|
||||||
|
foreach ($fonts as $font) {
|
||||||
|
$font_slug = sanitize_title($font['name']);
|
||||||
|
|
||||||
|
foreach ($font['variants'] as $variant) {
|
||||||
|
$weight = $variant['weight'];
|
||||||
|
$style = $variant['style'];
|
||||||
|
|
||||||
|
// Build filename
|
||||||
|
if (strpos($weight, ' ') !== false) {
|
||||||
|
$filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
|
||||||
|
} else {
|
||||||
|
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use REST API URL for font file
|
||||||
|
$font_url = rest_url('mlf/v1/font/' . $filename);
|
||||||
|
|
||||||
|
// Generate @font-face rule (format like Google Fonts)
|
||||||
|
$css .= "/* {$style} {$weight} */\n";
|
||||||
|
$css .= "@font-face {\n";
|
||||||
|
$css .= " font-family: '{$font['name']}';\n";
|
||||||
|
$css .= " font-style: {$style};\n";
|
||||||
|
$css .= " font-weight: {$weight};\n";
|
||||||
|
$css .= " font-display: swap;\n";
|
||||||
|
$css .= " src: url({$font_url}) format('woff2');\n";
|
||||||
|
$css .= "}\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new WP_REST_Response($css);
|
||||||
|
$response->set_status(200);
|
||||||
|
$response->set_headers([
|
||||||
|
'Content-Type' => 'text/css; charset=UTF-8',
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Cache-Control' => 'public, max-age=86400',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,20 @@ define('MLF_MAX_CSS_SIZE', 512 * 1024); // 512KB max CSS response
|
||||||
define('MLF_MAX_FONT_FILE_SIZE', 5 * 1024 * 1024); // 5MB max font file
|
define('MLF_MAX_FONT_FILE_SIZE', 5 * 1024 * 1024); // 5MB max font file
|
||||||
define('MLF_MAX_FONT_FACES', 20); // Max font faces per import (9 weights × 2 styles + buffer)
|
define('MLF_MAX_FONT_FACES', 20); // Max font faces per import (9 weights × 2 styles + buffer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure WOFF2 MIME type is registered (fixes Safari font loading).
|
||||||
|
*
|
||||||
|
* @param array $mimes Existing MIME types.
|
||||||
|
* @return array Modified MIME types.
|
||||||
|
*/
|
||||||
|
function mlf_add_woff2_mime_type($mimes) {
|
||||||
|
$mimes['woff2'] = 'font/woff2';
|
||||||
|
$mimes['woff'] = 'font/woff';
|
||||||
|
return $mimes;
|
||||||
|
}
|
||||||
|
add_filter('mime_types', 'mlf_add_woff2_mime_type');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check WordPress version on activation.
|
* Check WordPress version on activation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,9 +64,46 @@ function mlf_activate() {
|
||||||
if (!file_exists($font_dir['path'])) {
|
if (!file_exists($font_dir['path'])) {
|
||||||
wp_mkdir_p($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 .= "<IfModule mod_mime.c>\n";
|
||||||
|
$htaccess_content .= " AddType font/woff2 .woff2\n";
|
||||||
|
$htaccess_content .= " AddType font/woff .woff\n";
|
||||||
|
$htaccess_content .= "</IfModule>\n\n";
|
||||||
|
$htaccess_content .= "<IfModule mod_headers.c>\n";
|
||||||
|
$htaccess_content .= " <FilesMatch \"\\.(woff2?|ttf|otf|eot)$\">\n";
|
||||||
|
$htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n";
|
||||||
|
$htaccess_content .= " </FilesMatch>\n";
|
||||||
|
$htaccess_content .= "</IfModule>\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');
|
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).
|
* Check WordPress version on admin init (in case WP was downgraded).
|
||||||
*/
|
*/
|
||||||
|
|
@ -64,6 +115,44 @@ function mlf_check_version() {
|
||||||
}
|
}
|
||||||
add_action('admin_init', 'mlf_check_version');
|
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 .= "<IfModule mod_mime.c>\n";
|
||||||
|
$htaccess_content .= " AddType font/woff2 .woff2\n";
|
||||||
|
$htaccess_content .= " AddType font/woff .woff\n";
|
||||||
|
$htaccess_content .= "</IfModule>\n\n";
|
||||||
|
$htaccess_content .= "<IfModule mod_headers.c>\n";
|
||||||
|
$htaccess_content .= " <FilesMatch \"\\.(woff2?|ttf|otf|eot)$\">\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 .= " </FilesMatch>\n";
|
||||||
|
$htaccess_content .= "</IfModule>\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.
|
* Display version notice.
|
||||||
*/
|
*/
|
||||||
|
|
@ -133,9 +222,41 @@ add_action('plugins_loaded', 'mlf_init', 20);
|
||||||
function mlf_register_rest_routes() {
|
function mlf_register_rest_routes() {
|
||||||
$controller = new MLF_Rest_Controller();
|
$controller = new MLF_Rest_Controller();
|
||||||
$controller->register_routes();
|
$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');
|
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.
|
* Get the required capability for managing fonts.
|
||||||
*
|
*
|
||||||
|
|
@ -154,20 +275,24 @@ function mlf_get_capability() {
|
||||||
/**
|
/**
|
||||||
* Add imported fonts to the theme.json typography settings.
|
* Add imported fonts to the theme.json typography settings.
|
||||||
*
|
*
|
||||||
* This makes fonts appear in the Site Editor typography dropdown.
|
* 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.
|
* @param WP_Theme_JSON_Data $theme_json The theme.json data.
|
||||||
* @return WP_Theme_JSON_Data Modified theme.json data.
|
* @return WP_Theme_JSON_Data Modified theme.json data.
|
||||||
*/
|
*/
|
||||||
function mlf_add_fonts_to_theme_json($theme_json) {
|
function mlf_add_fonts_to_theme_json($theme_json) {
|
||||||
$registry = new MLF_Font_Registry();
|
$registry = new MLF_Font_Registry();
|
||||||
$fonts = $registry->get_imported_fonts();
|
$fonts = $registry->get_imported_fonts_with_src();
|
||||||
|
|
||||||
if (empty($fonts)) {
|
if (empty($fonts)) {
|
||||||
return $theme_json;
|
return $theme_json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build our font families
|
||||||
$font_families = [];
|
$font_families = [];
|
||||||
|
$font_dir = wp_get_font_dir();
|
||||||
|
$font_base_url = trailingslashit($font_dir['url']);
|
||||||
|
|
||||||
foreach ($fonts as $font) {
|
foreach ($fonts as $font) {
|
||||||
$font_faces = [];
|
$font_faces = [];
|
||||||
|
|
@ -175,23 +300,16 @@ function mlf_add_fonts_to_theme_json($theme_json) {
|
||||||
foreach ($font['variants'] as $variant) {
|
foreach ($font['variants'] as $variant) {
|
||||||
$weight = $variant['weight'];
|
$weight = $variant['weight'];
|
||||||
$style = $variant['style'];
|
$style = $variant['style'];
|
||||||
|
$filename = $variant['filename'];
|
||||||
|
|
||||||
// Build filename based on our naming convention
|
// Use direct file URL
|
||||||
$font_slug = sanitize_title($font['name']);
|
$font_url = $font_base_url . $filename;
|
||||||
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[] = [
|
$font_faces[] = [
|
||||||
'fontFamily' => $font['name'],
|
'fontFamily' => $font['name'],
|
||||||
'fontWeight' => $weight,
|
'fontWeight' => $weight,
|
||||||
'fontStyle' => $style,
|
'fontStyle' => $style,
|
||||||
|
'fontDisplay' => 'swap',
|
||||||
'src' => [$font_url],
|
'src' => [$font_url],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +322,7 @@ function mlf_add_fonts_to_theme_json($theme_json) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use update_with to merge - WordPress handles the merging logic
|
||||||
$new_data = [
|
$new_data = [
|
||||||
'version' => 2,
|
'version' => 2,
|
||||||
'settings' => [
|
'settings' => [
|
||||||
|
|
@ -215,7 +334,172 @@ function mlf_add_fonts_to_theme_json($theme_json) {
|
||||||
|
|
||||||
return $theme_json->update_with($new_data);
|
return $theme_json->update_with($new_data);
|
||||||
}
|
}
|
||||||
add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_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.
|
* Register admin menu.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue