287 lines
9.2 KiB
PHP
287 lines
9.2 KiB
PHP
<?php
|
||
/**
|
||
* Font Registry for Maple Local Fonts.
|
||
*
|
||
* @package Maple_Local_Fonts
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) {
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Class MLF_Font_Registry
|
||
*
|
||
* Handles registering fonts with WordPress Font Library API.
|
||
*/
|
||
class MLF_Font_Registry {
|
||
|
||
/**
|
||
* Register a font family with WordPress Font Library.
|
||
*
|
||
* @param string $font_name Display name (e.g., "Open Sans").
|
||
* @param string $font_slug Slug (e.g., "open-sans").
|
||
* @param array $files Array of downloaded file data.
|
||
* @return int|WP_Error Font family post ID or error.
|
||
*/
|
||
public function register_font($font_name, $font_slug, $files) {
|
||
// Check if font already exists
|
||
$existing = get_posts([
|
||
'post_type' => 'wp_font_family',
|
||
'name' => $font_slug,
|
||
'posts_per_page' => 1,
|
||
'post_status' => 'publish',
|
||
]);
|
||
|
||
if (!empty($existing)) {
|
||
return new WP_Error('font_exists', 'Font family already installed');
|
||
}
|
||
|
||
// Get font directory
|
||
$font_dir = wp_get_font_dir();
|
||
|
||
// Build font face array for WordPress
|
||
$font_faces = [];
|
||
foreach ($files as $file) {
|
||
$filename = basename($file['path']);
|
||
$font_faces[] = [
|
||
'fontFamily' => $font_name,
|
||
'fontWeight' => $file['weight'],
|
||
'fontStyle' => $file['style'],
|
||
'src' => 'file:./' . $filename,
|
||
];
|
||
}
|
||
|
||
// Build font family settings
|
||
$font_family_settings = [
|
||
'name' => $font_name,
|
||
'slug' => $font_slug,
|
||
'fontFamily' => sprintf('"%s", sans-serif', $font_name),
|
||
'fontFace' => $font_faces,
|
||
];
|
||
|
||
// Create font family post
|
||
$family_id = wp_insert_post([
|
||
'post_type' => 'wp_font_family',
|
||
'post_title' => $font_name,
|
||
'post_name' => $font_slug,
|
||
'post_status' => 'publish',
|
||
'post_content' => wp_json_encode($font_family_settings),
|
||
]);
|
||
|
||
if (is_wp_error($family_id)) {
|
||
return $family_id;
|
||
}
|
||
|
||
// Mark as imported by our plugin (for identification)
|
||
update_post_meta($family_id, '_mlf_imported', '1');
|
||
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
|
||
|
||
// Create font face posts (children)
|
||
foreach ($files as $file) {
|
||
$filename = basename($file['path']);
|
||
|
||
$face_settings = [
|
||
'fontFamily' => $font_name,
|
||
'fontWeight' => $file['weight'],
|
||
'fontStyle' => $file['style'],
|
||
'src' => 'file:./' . $filename,
|
||
];
|
||
|
||
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_status' => 'publish',
|
||
'post_content' => wp_json_encode($face_settings),
|
||
]);
|
||
}
|
||
|
||
// Clear font caches
|
||
delete_transient('wp_font_library_fonts');
|
||
delete_transient('mlf_imported_fonts_list');
|
||
|
||
return $family_id;
|
||
}
|
||
|
||
/**
|
||
* Delete a font family and its files.
|
||
*
|
||
* @param int $family_id Font family post ID.
|
||
* @return bool|WP_Error True on success, error on failure.
|
||
*/
|
||
public function delete_font($family_id) {
|
||
$family = get_post($family_id);
|
||
|
||
if (!$family || $family->post_type !== 'wp_font_family') {
|
||
return new WP_Error('not_found', 'Font family not found');
|
||
}
|
||
|
||
// Verify it's one we imported
|
||
if (get_post_meta($family_id, '_mlf_imported', true) !== '1') {
|
||
return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
|
||
}
|
||
|
||
// Get font faces (children)
|
||
$faces = get_children([
|
||
'post_parent' => $family_id,
|
||
'post_type' => 'wp_font_face',
|
||
]);
|
||
|
||
$font_dir = wp_get_font_dir();
|
||
|
||
// Delete font face files and posts
|
||
foreach ($faces as $face) {
|
||
$settings = json_decode($face->post_content, true);
|
||
|
||
if (isset($settings['src'])) {
|
||
// Convert file:. URL to path
|
||
$src = $settings['src'];
|
||
$src = str_replace('file:./', '', $src);
|
||
$file_path = trailingslashit($font_dir['path']) . basename($src);
|
||
|
||
// Validate path and extension before deletion
|
||
if ($this->validate_font_path($file_path)
|
||
&& pathinfo($file_path, PATHINFO_EXTENSION) === 'woff2'
|
||
&& file_exists($file_path)) {
|
||
wp_delete_file($file_path);
|
||
}
|
||
}
|
||
|
||
wp_delete_post($face->ID, true);
|
||
}
|
||
|
||
// Delete family post
|
||
wp_delete_post($family_id, true);
|
||
|
||
// Clear caches
|
||
delete_transient('wp_font_library_fonts');
|
||
delete_transient('mlf_imported_fonts_list');
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Get all fonts imported by this plugin.
|
||
*
|
||
* Uses optimized queries to avoid N+1 pattern.
|
||
*
|
||
* @return array Array of font data.
|
||
*/
|
||
public function get_imported_fonts() {
|
||
// Check transient cache first
|
||
$cached = get_transient('mlf_imported_fonts_list');
|
||
if ($cached !== false) {
|
||
return $cached;
|
||
}
|
||
|
||
$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)) {
|
||
set_transient('mlf_imported_fonts_list', [], 5 * MINUTE_IN_SECONDS);
|
||
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 (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_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;
|
||
}
|
||
|
||
// Batch get all import dates in single query
|
||
$import_dates = [];
|
||
foreach ($font_ids as $font_id) {
|
||
$import_dates[$font_id] = get_post_meta($font_id, '_mlf_import_date', true);
|
||
}
|
||
|
||
$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);
|
||
$variants[] = [
|
||
'weight' => $face_settings['fontWeight'] ?? '400',
|
||
'style' => $face_settings['fontStyle'] ?? 'normal',
|
||
];
|
||
}
|
||
|
||
// Sort variants by weight then style
|
||
usort($variants, function($a, $b) {
|
||
$weight_cmp = intval($a['weight']) - intval($b['weight']);
|
||
if ($weight_cmp !== 0) {
|
||
return $weight_cmp;
|
||
}
|
||
return strcmp($a['style'], $b['style']);
|
||
});
|
||
|
||
$result[] = [
|
||
'id' => $font->ID,
|
||
'name' => $settings['name'] ?? $font->post_title,
|
||
'slug' => $settings['slug'] ?? $font->post_name,
|
||
'variants' => $variants,
|
||
'import_date' => $import_dates[$font->ID] ?? '',
|
||
];
|
||
}
|
||
|
||
// Cache for 5 minutes
|
||
set_transient('mlf_imported_fonts_list', $result, 5 * MINUTE_IN_SECONDS);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Validate that a path is within the WordPress fonts directory.
|
||
*
|
||
* @param string $path Full path to validate.
|
||
* @return bool True if path is safe, false otherwise.
|
||
*/
|
||
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) {
|
||
return false;
|
||
}
|
||
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
|
||
} else {
|
||
$real_path = wp_normalize_path($real_path);
|
||
}
|
||
|
||
// Must be within fonts directory
|
||
return strpos($real_path, $fonts_path) === 0;
|
||
}
|
||
}
|