initial commit
This commit is contained in:
parent
d066133bd4
commit
e6f71e3706
55 changed files with 11928 additions and 0 deletions
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Page for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Admin_Page
|
||||
*
|
||||
* Handles the admin settings page rendering.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Empty constructor - class is instantiated for rendering
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the admin page.
|
||||
*/
|
||||
public function render() {
|
||||
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
|
||||
if (!current_user_can($capability)) {
|
||||
wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
$registry = new MLF_Font_Registry();
|
||||
$installed_fonts = $registry->get_imported_fonts();
|
||||
?>
|
||||
<div class="wrap mlf-wrap">
|
||||
<h1><?php esc_html_e('Maple Local Fonts', 'maple-local-fonts'); ?></h1>
|
||||
|
||||
<div class="mlf-container">
|
||||
<!-- Import Section -->
|
||||
<div class="mlf-section mlf-import-section">
|
||||
<h2><?php esc_html_e('Import from Google Fonts', 'maple-local-fonts'); ?></h2>
|
||||
|
||||
<form id="mlf-import-form" class="mlf-form">
|
||||
<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; ?>
|
||||
</div>
|
||||
</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>
|
||||
</label>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<!-- Installed Fonts Section -->
|
||||
<div class="mlf-section mlf-installed-section">
|
||||
<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>
|
||||
<?php else : ?>
|
||||
<div id="mlf-font-list" class="mlf-font-list">
|
||||
<?php foreach ($installed_fonts as $font) : ?>
|
||||
<div class="mlf-font-item" data-font-id="<?php echo esc_attr($font['id']); ?>">
|
||||
<div class="mlf-font-info">
|
||||
<h3 class="mlf-font-name"><?php echo esc_html($font['name']); ?></h3>
|
||||
<p class="mlf-font-variants">
|
||||
<?php
|
||||
$variant_strings = [];
|
||||
foreach ($font['variants'] as $variant) {
|
||||
$variant_strings[] = sprintf('%s %s', $variant['weight'], $variant['style']);
|
||||
}
|
||||
echo esc_html(implode(', ', $variant_strings));
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mlf-font-actions">
|
||||
<button type="button" class="button mlf-delete-btn" data-font-id="<?php echo esc_attr($font['id']); ?>">
|
||||
<?php esc_html_e('Delete', 'maple-local-fonts'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<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>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="mlf-info-box mlf-info-box-classic">
|
||||
<span class="dashicons dashicons-info"></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>
|
||||
<pre class="mlf-code-example">body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
}</pre>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: link to Customizer */
|
||||
esc_html__('Add this CSS in %s or your theme\'s style.css file.', 'maple-local-fonts'),
|
||||
'<a href="' . esc_url(admin_url('customize.php')) . '">' . esc_html__('Appearance → Customize → Additional CSS', 'maple-local-fonts') . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
/**
|
||||
* AJAX Handler for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Ajax_Handler
|
||||
*
|
||||
* Handles all AJAX requests for font download and deletion.
|
||||
*/
|
||||
class MLF_Ajax_Handler {
|
||||
|
||||
/**
|
||||
* Rate limiter instance.
|
||||
*
|
||||
* @var MLF_Rate_Limiter
|
||||
*/
|
||||
private $rate_limiter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
|
||||
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
|
||||
// NEVER add wp_ajax_nopriv_ - admin only functionality
|
||||
|
||||
// Initialize rate limiter: 10 requests per minute
|
||||
$this->rate_limiter = new MLF_Rate_Limiter(10, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle font download AJAX request.
|
||||
*/
|
||||
public function handle_download() {
|
||||
// 1. NONCE CHECK
|
||||
if (!check_ajax_referer('mlf_download_font', '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('download')) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 4. INPUT VALIDATION
|
||||
// Validate font name
|
||||
$font_name = isset($_POST['font_name']) ? sanitize_text_field(wp_unslash($_POST['font_name'])) : '';
|
||||
|
||||
if (empty($font_name)) {
|
||||
wp_send_json_error(['message' => __('Font name is required.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Strict allowlist pattern - alphanumeric, spaces, hyphens only
|
||||
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
|
||||
wp_send_json_error(['message' => __('Invalid font name: only letters, numbers, spaces, and hyphens allowed.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
if (strlen($font_name) > 100) {
|
||||
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')]);
|
||||
}
|
||||
|
||||
// 5. PROCESS REQUEST
|
||||
try {
|
||||
$downloader = new MLF_Font_Downloader();
|
||||
$download_result = $downloader->download($font_name, $weights, $styles);
|
||||
|
||||
if (is_wp_error($download_result)) {
|
||||
wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
|
||||
}
|
||||
|
||||
// Register font with WordPress
|
||||
$registry = new MLF_Font_Registry();
|
||||
$result = $registry->register_font(
|
||||
$download_result['font_name'],
|
||||
$download_result['font_slug'],
|
||||
$download_result['files']
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $this->get_user_error_message($result)]);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => sprintf(
|
||||
/* translators: %s: font name */
|
||||
__('Successfully installed %s.', 'maple-local-fonts'),
|
||||
esc_html($font_name)
|
||||
),
|
||||
'font_id' => $result,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// Sanitize exception message before logging (defense in depth)
|
||||
error_log('MLF Download Error: ' . sanitize_text_field($e->getMessage()));
|
||||
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle font deletion AJAX request.
|
||||
*/
|
||||
public function handle_delete() {
|
||||
// 1. NONCE CHECK
|
||||
if (!check_ajax_referer('mlf_delete_font', '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('delete')) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 4. INPUT VALIDATION
|
||||
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
|
||||
|
||||
if ($font_id < 1) {
|
||||
wp_send_json_error(['message' => __('Invalid font ID.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Verify font exists and is a font family
|
||||
$font = get_post($font_id);
|
||||
if (!$font || $font->post_type !== 'wp_font_family') {
|
||||
wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Verify it's one we imported (not a theme font)
|
||||
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
|
||||
wp_send_json_error(['message' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// 5. PROCESS REQUEST
|
||||
try {
|
||||
$registry = new MLF_Font_Registry();
|
||||
$result = $registry->delete_font($font_id);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $this->get_user_error_message($result)]);
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => __('Font deleted successfully.', 'maple-local-fonts')]);
|
||||
} catch (Exception $e) {
|
||||
// Sanitize exception message before logging (defense in depth)
|
||||
error_log('MLF Delete Error: ' . sanitize_text_field($e->getMessage()));
|
||||
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert internal error codes to user-friendly messages.
|
||||
*
|
||||
* @param WP_Error $error The error object.
|
||||
* @return string User-friendly message.
|
||||
*/
|
||||
private function get_user_error_message($error) {
|
||||
$code = $error->get_error_code();
|
||||
|
||||
$messages = [
|
||||
'font_not_found' => __('Font not found on Google Fonts. Please check the spelling and try again.', 'maple-local-fonts'),
|
||||
'font_exists' => __('This font is already installed.', 'maple-local-fonts'),
|
||||
'request_failed' => __('Could not connect to Google Fonts. Please check your internet connection and try again.', 'maple-local-fonts'),
|
||||
'http_error' => __('Google Fonts returned an error. Please try again later.', 'maple-local-fonts'),
|
||||
'parse_failed' => __('Could not process the font data. The font may not be available.', 'maple-local-fonts'),
|
||||
'download_failed' => __('Could not download the font files. Please try again.', 'maple-local-fonts'),
|
||||
'write_failed' => __('Could not save font files. Please check that wp-content/fonts is writable.', 'maple-local-fonts'),
|
||||
'mkdir_failed' => __('Could not create fonts directory. Please check file permissions.', 'maple-local-fonts'),
|
||||
'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'),
|
||||
'file_too_large' => __('The font file is too large to download.', 'maple-local-fonts'),
|
||||
];
|
||||
|
||||
return $messages[$code] ?? __('An unexpected error occurred. Please try again.', 'maple-local-fonts');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,504 @@
|
|||
<?php
|
||||
/**
|
||||
* Font Downloader for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Font_Downloader
|
||||
*
|
||||
* Handles downloading fonts from Google Fonts CSS2 API.
|
||||
*/
|
||||
class MLF_Font_Downloader {
|
||||
|
||||
/**
|
||||
* User agent to send with requests (needed to get WOFF2 format).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
public function download($font_name, $weights, $styles) {
|
||||
// Validate inputs
|
||||
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');
|
||||
}
|
||||
|
||||
$styles = array_intersect($styles, ['normal', 'italic']);
|
||||
if (empty($styles)) {
|
||||
return new WP_Error('invalid_styles', 'No valid styles specified');
|
||||
}
|
||||
|
||||
// Fetch CSS from Google
|
||||
$css = $this->fetch_css($font_name, $weights, $styles);
|
||||
if (is_wp_error($css)) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
// Parse CSS to get font face data
|
||||
$font_faces = $this->parse_css($css, $font_name);
|
||||
if (is_wp_error($font_faces)) {
|
||||
return $font_faces;
|
||||
}
|
||||
|
||||
// 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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Google Fonts CSS2 API 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 +)
|
||||
$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) {
|
||||
$variations[] = "0,{$weight}";
|
||||
}
|
||||
if ($has_italic) {
|
||||
$variations[] = "1,{$weight}";
|
||||
}
|
||||
}
|
||||
|
||||
$variation_string = implode(';', $variations);
|
||||
return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch 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_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 $font_name Expected font family name.
|
||||
* @return array|WP_Error Array of font face data or error.
|
||||
*/
|
||||
private function parse_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');
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $block) {
|
||||
$face_data = $this->parse_font_face_block($block);
|
||||
|
||||
if (is_wp_error($face_data)) {
|
||||
continue; // Skip malformed blocks
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Limit number of font faces to prevent excessive downloads
|
||||
$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);
|
||||
}
|
||||
|
||||
return $font_faces_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single @font-face block.
|
||||
*
|
||||
* @param string $block Content inside @font-face { }.
|
||||
* @return array|WP_Error Parsed data or error.
|
||||
*/
|
||||
private function parse_font_face_block($block) {
|
||||
$data = [];
|
||||
|
||||
// Extract font-family
|
||||
if (preg_match('/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $m)) {
|
||||
$data['family'] = trim($m[1]);
|
||||
} else {
|
||||
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];
|
||||
} else {
|
||||
return new WP_Error('missing_weight', 'Missing font-weight');
|
||||
}
|
||||
|
||||
// Extract font-style
|
||||
if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) {
|
||||
$data['style'] = $m[1];
|
||||
} else {
|
||||
$data['style'] = 'normal'; // Default
|
||||
}
|
||||
|
||||
// Extract src URL - MUST be fonts.gstatic.com
|
||||
if (preg_match('/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/i', $block, $m)) {
|
||||
$data['url'] = $m[1];
|
||||
} else {
|
||||
return new WP_Error('missing_src', 'Missing or invalid src URL');
|
||||
}
|
||||
|
||||
// Extract unicode-range (optional, for subset detection)
|
||||
if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) {
|
||||
$data['unicode_range'] = trim($m[1]);
|
||||
} else {
|
||||
$data['unicode_range'] = '';
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if unicode-range indicates latin subset.
|
||||
*
|
||||
* @param string $range Unicode range string.
|
||||
* @return bool True if appears to be latin subset.
|
||||
*/
|
||||
private function is_latin_subset($range) {
|
||||
if (empty($range)) {
|
||||
return true; // Assume latin if no range specified
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download all font files.
|
||||
*
|
||||
* @param array $font_faces Array of font face data.
|
||||
* @param string $font_slug Font slug for filename.
|
||||
* @return array|WP_Error Array of downloaded file info or error.
|
||||
*/
|
||||
private function download_files($font_faces, $font_slug) {
|
||||
$downloaded = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($font_faces as $face) {
|
||||
$result = $this->download_single_file(
|
||||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
$face['style']
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
$errors[] = $result->get_error_message();
|
||||
continue;
|
||||
}
|
||||
|
||||
$downloaded[] = [
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => $face['style'],
|
||||
];
|
||||
}
|
||||
|
||||
// If no files downloaded, return error
|
||||
if (empty($downloaded)) {
|
||||
return new WP_Error(
|
||||
'download_failed',
|
||||
'Could not download any font files: ' . implode(', ', $errors)
|
||||
);
|
||||
}
|
||||
|
||||
return $downloaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single WOFF2 file from Google Fonts.
|
||||
*
|
||||
* @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.
|
||||
* @return string|WP_Error Local file path or error.
|
||||
*/
|
||||
private function download_single_file($url, $font_slug, $weight, $style) {
|
||||
// Validate URL is from Google
|
||||
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);
|
||||
$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,
|
||||
'user-agent' => $this->user_agent,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('download_failed', 'Failed to download font file: ' . $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);
|
||||
}
|
||||
|
||||
$content = wp_remote_retrieve_body($response);
|
||||
if (empty($content)) {
|
||||
return new WP_Error('empty_file', 'Downloaded font 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');
|
||||
}
|
||||
|
||||
// Verify it looks like a WOFF2 file (magic bytes: wOF2)
|
||||
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';
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
|
||||
return new WP_Error('write_failed', 'Could not write font file');
|
||||
}
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a URL is a legitimate Google Fonts URL.
|
||||
*
|
||||
* @param string $url URL to validate.
|
||||
* @return bool True if valid Google Fonts URL.
|
||||
*/
|
||||
private function is_valid_google_fonts_url($url) {
|
||||
$parsed = wp_parse_url($url);
|
||||
|
||||
if (!$parsed || !isset($parsed['host'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow Google Fonts domains
|
||||
$allowed_hosts = [
|
||||
'fonts.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
];
|
||||
|
||||
return in_array($parsed['host'], $allowed_hosts, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and validate a font filename.
|
||||
*
|
||||
* @param string $filename The filename to validate.
|
||||
* @return string|false Sanitized filename or false if invalid.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Directory doesn't exist yet, check parent
|
||||
$parent_dir = dirname($dir);
|
||||
$real_parent = realpath($parent_dir);
|
||||
if ($real_parent === false) {
|
||||
return false;
|
||||
}
|
||||
$real_path = wp_normalize_path($real_parent . '/' . basename($dir) . '/' . basename($path));
|
||||
} else {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
/**
|
||||
* Rate Limiter for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Rate_Limiter
|
||||
*
|
||||
* Provides rate limiting functionality to prevent abuse of AJAX endpoints.
|
||||
*/
|
||||
class MLF_Rate_Limiter {
|
||||
|
||||
/**
|
||||
* Default rate limit (requests per window).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $limit = 10;
|
||||
|
||||
/**
|
||||
* Default time window in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $window = 60;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $limit Maximum requests allowed per window.
|
||||
* @param int $window Time window in seconds.
|
||||
*/
|
||||
public function __construct($limit = 10, $window = 60) {
|
||||
$this->limit = absint($limit);
|
||||
$this->window = absint($window);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user/IP is rate limited.
|
||||
*
|
||||
* @param string $action The action being rate limited.
|
||||
* @return bool True if rate limited (should block), false if allowed.
|
||||
*/
|
||||
public function is_limited($action) {
|
||||
$key = $this->get_rate_limit_key($action);
|
||||
$data = get_transient($key);
|
||||
|
||||
if ($data === false) {
|
||||
// First request, not limited
|
||||
return false;
|
||||
}
|
||||
|
||||
return $data['count'] >= $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a request for rate limiting purposes.
|
||||
*
|
||||
* @param string $action The action being recorded.
|
||||
* @return void
|
||||
*/
|
||||
public function record_request($action) {
|
||||
$key = $this->get_rate_limit_key($action);
|
||||
$data = get_transient($key);
|
||||
|
||||
if ($data === false) {
|
||||
// First request in this window
|
||||
$data = [
|
||||
'count' => 1,
|
||||
'start' => time(),
|
||||
];
|
||||
} else {
|
||||
$data['count']++;
|
||||
}
|
||||
|
||||
// Set/update transient with remaining window time
|
||||
$elapsed = time() - $data['start'];
|
||||
$remaining = max(1, $this->window - $elapsed);
|
||||
|
||||
set_transient($key, $data, $remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit and record request in one call.
|
||||
*
|
||||
* @param string $action The action being checked.
|
||||
* @return bool True if request is allowed, false if rate limited.
|
||||
*/
|
||||
public function check_and_record($action) {
|
||||
if ($this->is_limited($action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->record_request($action);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of remaining requests in the current window.
|
||||
*
|
||||
* @param string $action The action to check.
|
||||
* @return int Number of remaining requests.
|
||||
*/
|
||||
public function get_remaining($action) {
|
||||
$key = $this->get_rate_limit_key($action);
|
||||
$data = get_transient($key);
|
||||
|
||||
if ($data === false) {
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
return max(0, $this->limit - $data['count']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limit key for the current user/IP.
|
||||
*
|
||||
* @param string $action The action being rate limited.
|
||||
* @return string Transient key.
|
||||
*/
|
||||
private function get_rate_limit_key($action) {
|
||||
// Use user ID if logged in, otherwise IP
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ($user_id > 0) {
|
||||
$identifier = 'user_' . $user_id;
|
||||
} else {
|
||||
// Sanitize and hash IP for privacy
|
||||
$ip = $this->get_client_ip();
|
||||
$identifier = 'ip_' . md5($ip);
|
||||
}
|
||||
|
||||
return 'mlf_rate_' . sanitize_key($action) . '_' . $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client IP address.
|
||||
*
|
||||
* @return string Client IP address.
|
||||
*/
|
||||
private function get_client_ip() {
|
||||
$ip = '';
|
||||
|
||||
// Check for various headers (in order of reliability)
|
||||
$headers = [
|
||||
'HTTP_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_X_CLUSTER_CLIENT_IP',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR',
|
||||
];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
// X-Forwarded-For can contain multiple IPs, get the first one
|
||||
$ips = explode(',', sanitize_text_field(wp_unslash($_SERVER[$header])));
|
||||
$ip = trim($ips[0]);
|
||||
|
||||
// Validate IP
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localhost if no valid IP found
|
||||
return $ip ?: '127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear rate limit for a specific action and user/IP.
|
||||
*
|
||||
* @param string $action The action to clear.
|
||||
* @return void
|
||||
*/
|
||||
public function clear($action) {
|
||||
$key = $this->get_rate_limit_key($action);
|
||||
delete_transient($key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,525 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Controller for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Rest_Controller
|
||||
*
|
||||
* Provides REST API endpoints for font management.
|
||||
*/
|
||||
class MLF_Rest_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Namespace for the API.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'mlf/v1';
|
||||
|
||||
/**
|
||||
* Resource name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'fonts';
|
||||
|
||||
/**
|
||||
* Rate limiter instance.
|
||||
*
|
||||
* @var MLF_Rate_Limiter
|
||||
*/
|
||||
private $rate_limiter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->rate_limiter = new MLF_Rate_Limiter(10, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes for the controller.
|
||||
*/
|
||||
public function register_routes() {
|
||||
// GET /wp-json/mlf/v1/fonts - List all fonts
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_items'],
|
||||
'permission_callback' => [$this, 'get_items_permissions_check'],
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'create_item'],
|
||||
'permission_callback' => [$this, 'create_item_permissions_check'],
|
||||
'args' => $this->get_create_item_args(),
|
||||
],
|
||||
'schema' => [$this, 'get_public_item_schema'],
|
||||
]
|
||||
);
|
||||
|
||||
// DELETE /wp-json/mlf/v1/fonts/{id} - Delete a font
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => [$this, 'delete_item'],
|
||||
'permission_callback' => [$this, 'delete_item_permissions_check'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'description' => __('Unique identifier for the font.', 'maple-local-fonts'),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// GET /wp-json/mlf/v1/fonts/{id} - Get a single font
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_item'],
|
||||
'permission_callback' => [$this, 'get_items_permissions_check'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'description' => __('Unique identifier for the font.', 'maple-local-fonts'),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given request has access to get items.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return bool|WP_Error True if the request has read access, WP_Error object otherwise.
|
||||
*/
|
||||
public function get_items_permissions_check($request) {
|
||||
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
|
||||
if (!current_user_can($capability)) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Sorry, you are not allowed to view fonts.', 'maple-local-fonts'),
|
||||
['status' => rest_authorization_required_code()]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given request has access to create items.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return bool|WP_Error True if the request has create access, WP_Error object otherwise.
|
||||
*/
|
||||
public function create_item_permissions_check($request) {
|
||||
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
|
||||
if (!current_user_can($capability)) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Sorry, you are not allowed to create fonts.', 'maple-local-fonts'),
|
||||
['status' => rest_authorization_required_code()]
|
||||
);
|
||||
}
|
||||
|
||||
// Rate limit check
|
||||
if (!$this->rate_limiter->check_and_record('rest_create')) {
|
||||
return new WP_Error(
|
||||
'rest_rate_limited',
|
||||
__('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
|
||||
['status' => 429]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given request has access to delete a specific item.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return bool|WP_Error True if the request has delete access, WP_Error object otherwise.
|
||||
*/
|
||||
public function delete_item_permissions_check($request) {
|
||||
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
|
||||
if (!current_user_can($capability)) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Sorry, you are not allowed to delete fonts.', 'maple-local-fonts'),
|
||||
['status' => rest_authorization_required_code()]
|
||||
);
|
||||
}
|
||||
|
||||
// Rate limit check
|
||||
if (!$this->rate_limiter->check_and_record('rest_delete')) {
|
||||
return new WP_Error(
|
||||
'rest_rate_limited',
|
||||
__('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
|
||||
['status' => 429]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of fonts.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_items($request) {
|
||||
$registry = new MLF_Font_Registry();
|
||||
$fonts = $registry->get_imported_fonts();
|
||||
|
||||
$data = [];
|
||||
foreach ($fonts as $font) {
|
||||
$data[] = $this->prepare_item_for_response($font, $request);
|
||||
}
|
||||
|
||||
return rest_ensure_response($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single font.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item($request) {
|
||||
$font_id = absint($request->get_param('id'));
|
||||
$font = get_post($font_id);
|
||||
|
||||
if (!$font || $font->post_type !== 'wp_font_family') {
|
||||
return new WP_Error(
|
||||
'rest_font_not_found',
|
||||
__('Font not found.', 'maple-local-fonts'),
|
||||
['status' => 404]
|
||||
);
|
||||
}
|
||||
|
||||
// Verify it's one we imported
|
||||
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
|
||||
return new WP_Error(
|
||||
'rest_font_not_found',
|
||||
__('Font not found.', 'maple-local-fonts'),
|
||||
['status' => 404]
|
||||
);
|
||||
}
|
||||
|
||||
$registry = new MLF_Font_Registry();
|
||||
$fonts = $registry->get_imported_fonts();
|
||||
$font_data = null;
|
||||
|
||||
foreach ($fonts as $f) {
|
||||
if ($f['id'] === $font_id) {
|
||||
$font_data = $f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$font_data) {
|
||||
return new WP_Error(
|
||||
'rest_font_not_found',
|
||||
__('Font not found.', 'maple-local-fonts'),
|
||||
['status' => 404]
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response($this->prepare_item_for_response($font_data, $request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a font.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
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'));
|
||||
|
||||
// Validate font name
|
||||
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('Invalid font name: only letters, numbers, spaces, and hyphens allowed.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
if (strlen($font_name) > 100) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('Font name is too long.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (is_wp_error($download_result)) {
|
||||
return new WP_Error(
|
||||
'rest_download_failed',
|
||||
$download_result->get_error_message(),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Register font with WordPress
|
||||
$registry = new MLF_Font_Registry();
|
||||
$result = $registry->register_font(
|
||||
$download_result['font_name'],
|
||||
$download_result['font_slug'],
|
||||
$download_result['files']
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return new WP_Error(
|
||||
'rest_register_failed',
|
||||
$result->get_error_message(),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Return the created font
|
||||
$fonts = $registry->get_imported_fonts();
|
||||
$created_font = null;
|
||||
foreach ($fonts as $font) {
|
||||
if ($font['id'] === $result) {
|
||||
$created_font = $font;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$response = rest_ensure_response($this->prepare_item_for_response($created_font, $request));
|
||||
$response->set_status(201);
|
||||
$response->header('Location', rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $result)));
|
||||
|
||||
return $response;
|
||||
} catch (Exception $e) {
|
||||
error_log('MLF REST Create Error: ' . sanitize_text_field($e->getMessage()));
|
||||
return new WP_Error(
|
||||
'rest_internal_error',
|
||||
__('An unexpected error occurred.', 'maple-local-fonts'),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a font.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function delete_item($request) {
|
||||
$font_id = absint($request->get_param('id'));
|
||||
$font = get_post($font_id);
|
||||
|
||||
if (!$font || $font->post_type !== 'wp_font_family') {
|
||||
return new WP_Error(
|
||||
'rest_font_not_found',
|
||||
__('Font not found.', 'maple-local-fonts'),
|
||||
['status' => 404]
|
||||
);
|
||||
}
|
||||
|
||||
// Verify it's one we imported
|
||||
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
|
||||
return new WP_Error(
|
||||
'rest_cannot_delete',
|
||||
__('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'),
|
||||
['status' => 403]
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$registry = new MLF_Font_Registry();
|
||||
$result = $registry->delete_font($font_id);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return new WP_Error(
|
||||
'rest_delete_failed',
|
||||
$result->get_error_message(),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response([
|
||||
'deleted' => true,
|
||||
'message' => __('Font deleted successfully.', 'maple-local-fonts'),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('MLF REST Delete Error: ' . sanitize_text_field($e->getMessage()));
|
||||
return new WP_Error(
|
||||
'rest_internal_error',
|
||||
__('An unexpected error occurred.', 'maple-local-fonts'),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a font for the REST response.
|
||||
*
|
||||
* @param array $font Font data.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return array Prepared font data.
|
||||
*/
|
||||
public function prepare_item_for_response($font, $request) {
|
||||
return [
|
||||
'id' => $font['id'],
|
||||
'name' => $font['name'],
|
||||
'slug' => $font['slug'],
|
||||
'variants' => $font['variants'],
|
||||
'_links' => [
|
||||
'self' => [
|
||||
['href' => rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $font['id']))],
|
||||
],
|
||||
'collection' => [
|
||||
['href' => rest_url(sprintf('%s/%s', $this->namespace, $this->rest_base))],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument schema for creating items.
|
||||
*
|
||||
* @return array Arguments schema.
|
||||
*/
|
||||
protected function get_create_item_args() {
|
||||
return [
|
||||
'font_name' => [
|
||||
'description' => __('The font family name from Google Fonts.', 'maple-local-fonts'),
|
||||
'type' => 'string',
|
||||
'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'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the font schema.
|
||||
*
|
||||
* @return array Schema definition.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return [
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'font',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => [
|
||||
'description' => __('Unique identifier for the font.', 'maple-local-fonts'),
|
||||
'type' => 'integer',
|
||||
'context' => ['view'],
|
||||
'readonly' => true,
|
||||
],
|
||||
'name' => [
|
||||
'description' => __('The font family name.', 'maple-local-fonts'),
|
||||
'type' => 'string',
|
||||
'context' => ['view'],
|
||||
],
|
||||
'slug' => [
|
||||
'description' => __('The font slug.', 'maple-local-fonts'),
|
||||
'type' => 'string',
|
||||
'context' => ['view'],
|
||||
],
|
||||
'variants' => [
|
||||
'description' => __('Array of font variants.', 'maple-local-fonts'),
|
||||
'type' => 'array',
|
||||
'context' => ['view'],
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'weight' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'style' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
5
native/wordpress/maple-fonts-wp/includes/index.php
Normal file
5
native/wordpress/maple-fonts-wp/includes/index.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue