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'); } }