monorepo/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php
2026-02-02 08:31:36 -05:00

372 lines
15 KiB
PHP

<?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']);
add_action('wp_ajax_mlf_check_updates', [$this, 'handle_check_updates']);
add_action('wp_ajax_mlf_update_font', [$this, 'handle_update_font']);
// 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 include_italic (boolean)
$include_italic = isset($_POST['include_italic']) && $_POST['include_italic'] === '1';
// Get version info (optional)
$font_version = isset($_POST['font_version']) ? sanitize_text_field(wp_unslash($_POST['font_version'])) : '';
$font_last_modified = isset($_POST['font_last_modified']) ? sanitize_text_field(wp_unslash($_POST['font_last_modified'])) : '';
// 5. PROCESS REQUEST
try {
$downloader = new MLF_Font_Downloader();
$download_result = $downloader->download($font_name, $include_italic);
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'],
$font_version,
$font_last_modified
);
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')]);
}
}
/**
* Handle check for updates AJAX request.
*/
public function handle_check_updates() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_check_updates', '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('check_updates')) {
wp_send_json_error([
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
], 429);
}
// 4. GET INSTALLED FONTS AND CHECK VERSIONS
try {
$registry = new MLF_Font_Registry();
$installed_fonts = $registry->get_imported_fonts();
if (empty($installed_fonts)) {
wp_send_json_success(['updates' => []]);
}
$font_search = new MLF_Font_Search();
$updates = [];
foreach ($installed_fonts as $font) {
$current_version = $font_search->get_font_version($font['name']);
if ($current_version && !empty($current_version['version'])) {
$installed_version = $font['version'] ?? '';
// Compare versions
if (!empty($installed_version) && $installed_version !== $current_version['version']) {
$updates[$font['id']] = [
'installed_version' => $installed_version,
'latest_version' => $current_version['version'],
'last_modified' => $current_version['lastModified'],
];
} elseif (empty($installed_version)) {
// No version stored, consider it needs update
$updates[$font['id']] = [
'installed_version' => '',
'latest_version' => $current_version['version'],
'last_modified' => $current_version['lastModified'],
];
}
}
}
wp_send_json_success(['updates' => $updates]);
} catch (Exception $e) {
error_log('MLF Check Updates Error: ' . sanitize_text_field($e->getMessage()));
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
}
}
/**
* Handle font update (re-download) AJAX request.
*/
public function handle_update_font() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_update_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('update')) {
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 one we imported
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]);
}
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => __('Cannot update fonts not imported by this plugin.', 'maple-local-fonts')]);
}
// Get font name from post content
$settings = json_decode($font->post_content, true);
$font_name = $settings['name'] ?? $font->post_title;
// 5. DELETE OLD FONT AND RE-DOWNLOAD
try {
$registry = new MLF_Font_Registry();
// Delete old font
$delete_result = $registry->delete_font($font_id);
if (is_wp_error($delete_result)) {
wp_send_json_error(['message' => $this->get_user_error_message($delete_result)]);
}
// Get latest version info
$font_search = new MLF_Font_Search();
$version_info = $font_search->get_font_version($font_name);
$font_version = $version_info['version'] ?? '';
$font_last_modified = $version_info['lastModified'] ?? '';
// Re-download font (include italic by default)
$downloader = new MLF_Font_Downloader();
$download_result = $downloader->download($font_name, true);
if (is_wp_error($download_result)) {
wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
}
// Register font with new version info
$result = $registry->register_font(
$download_result['font_name'],
$download_result['font_slug'],
$download_result['files'],
$font_version,
$font_last_modified
);
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 updated %s.', 'maple-local-fonts'),
esc_html($font_name)
),
'font_id' => $result,
'version' => $font_version,
]);
} catch (Exception $e) {
error_log('MLF Update Font 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'),
'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.', 'maple-local-fonts'),
'file_too_large' => __('The font file is too large to download.', 'maple-local-fonts'),
'no_variable' => __('Variable font not available, trying static fonts...', 'maple-local-fonts'),
'no_fonts' => __('No font files found. The font may not support the requested styles.', 'maple-local-fonts'),
];
return $messages[$code] ?? __('An unexpected error occurred. Please try again.', 'maple-local-fonts');
}
}