monorepo/native/wordpress/maple-fonts-wp/includes/class-mlf-rest-controller.php
2026-01-30 22:33:40 -05:00

525 lines
18 KiB
PHP

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