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[\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[\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', ], ], ], ], ], ]; } }