597 lines
18 KiB
PHP
597 lines
18 KiB
PHP
<?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) {
|
||
$registry = new MLF_Font_Registry();
|
||
$fonts = $registry->get_imported_fonts_with_src();
|
||
|
||
if (empty($fonts)) {
|
||
return $theme_json;
|
||
}
|
||
|
||
// Build our font families
|
||
$font_families = [];
|
||
$font_dir = wp_get_font_dir();
|
||
$font_base_url = trailingslashit($font_dir['url']);
|
||
|
||
foreach ($fonts as $font) {
|
||
$font_faces = [];
|
||
|
||
foreach ($font['variants'] as $variant) {
|
||
$weight = $variant['weight'];
|
||
$style = $variant['style'];
|
||
$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],
|
||
];
|
||
}
|
||
|
||
$font_families[] = [
|
||
'name' => $font['name'],
|
||
'slug' => $font['slug'],
|
||
'fontFamily' => "'{$font['name']}', sans-serif",
|
||
'fontFace' => $font_faces,
|
||
];
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
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');
|