initial commit
This commit is contained in:
parent
d066133bd4
commit
e6f71e3706
55 changed files with 11928 additions and 0 deletions
|
|
@ -0,0 +1,525 @@
|
|||
<?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',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue