525 lines
18 KiB
PHP
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',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
}
|