monorepo/native/wordpress/maple-fonts-wp/maple-local-fonts.php
2026-02-02 11:37:40 -05:00

627 lines
20 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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