You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
708 lines
18 KiB
PHP
708 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Class Font_Controller
|
|
*
|
|
* @link https://github.com/googleforcreators/web-stories-wp
|
|
*
|
|
* @copyright 2021 Google LLC
|
|
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
|
*/
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
declare(strict_types = 1);
|
|
|
|
namespace Google\Web_Stories\REST_API;
|
|
|
|
use stdClass;
|
|
use WP_Error;
|
|
use WP_Post;
|
|
use WP_Query;
|
|
use WP_REST_Posts_Controller;
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_REST_Server;
|
|
|
|
/**
|
|
* Font_Controller class.
|
|
*
|
|
* @phpstan-type Font array{
|
|
* family: string,
|
|
* fallbacks?: string[],
|
|
* weights?: array<int, array{0: int, 1: int}>,
|
|
* styles?: string[],
|
|
* variants?: string[],
|
|
* service?: string,
|
|
* metrics?: mixed,
|
|
* id?: string,
|
|
* url?: string
|
|
* }
|
|
* @phpstan-type SchemaEntry array{
|
|
* description: string,
|
|
* type: string,
|
|
* context: string[],
|
|
* default?: mixed,
|
|
* }
|
|
* @phpstan-type Schema array{
|
|
* properties: array{
|
|
* family?: SchemaEntry,
|
|
* fallbacks?: SchemaEntry,
|
|
* weights?: SchemaEntry,
|
|
* styles?: SchemaEntry,
|
|
* variants?: SchemaEntry,
|
|
* service?: SchemaEntry,
|
|
* metrics?: SchemaEntry,
|
|
* id?: SchemaEntry,
|
|
* url?: SchemaEntry
|
|
* }
|
|
* }
|
|
*/
|
|
class Font_Controller extends WP_REST_Posts_Controller {
|
|
/**
|
|
* Registers the routes for posts.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @see register_rest_route()
|
|
*/
|
|
public function register_routes(): void {
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base,
|
|
[
|
|
[
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [ $this, 'get_items' ],
|
|
'permission_callback' => [ $this, 'get_items_permissions_check' ],
|
|
'args' => $this->get_collection_params(),
|
|
],
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [ $this, 'create_item' ],
|
|
'permission_callback' => [ $this, 'create_item_permissions_check' ],
|
|
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
|
|
],
|
|
'schema' => [ $this, 'get_public_item_schema' ],
|
|
]
|
|
);
|
|
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
|
[
|
|
'args' => [
|
|
'id' => [
|
|
'description' => __( 'Unique identifier for the font.', 'web-stories' ),
|
|
'type' => 'integer',
|
|
],
|
|
],
|
|
[
|
|
'methods' => WP_REST_Server::DELETABLE,
|
|
'callback' => [ $this, 'delete_item' ],
|
|
'permission_callback' => [ $this, 'delete_item_permissions_check' ],
|
|
],
|
|
'schema' => [ $this, 'get_public_item_schema' ],
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Retrieves a collection of fonts.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param WP_REST_Request $request Full details about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function get_items( $request ) {
|
|
/**
|
|
* Fonts list.
|
|
*
|
|
* @phpstan-var Font[] $fonts
|
|
*/
|
|
$fonts = [];
|
|
|
|
// Retrieve the list of registered collection query parameters.
|
|
$registered = $this->get_collection_params();
|
|
|
|
if ( isset( $registered['service'], $request['service'] ) ) {
|
|
if ( 'all' === $request['service'] || 'builtin' === $request['service'] ) {
|
|
array_push( $fonts, ...$this->get_builtin_fonts() );
|
|
|
|
// For custom fonts the searching will be done in WP_Query already.
|
|
if ( isset( $registered['search'], $request['search'] ) && ! empty( $request['search'] ) ) {
|
|
/**
|
|
* Requested URL.
|
|
*
|
|
* @var string $search
|
|
*/
|
|
$search = $request['search'];
|
|
$fonts = array_values(
|
|
array_filter(
|
|
$fonts,
|
|
/**
|
|
* Font data.
|
|
*
|
|
* @param array{family: string} $font
|
|
* @return bool
|
|
*/
|
|
static fn( array $font ) => false !== stripos( $font['family'], $search )
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( 'all' === $request['service'] || 'custom' === $request['service'] ) {
|
|
array_push( $fonts, ...$this->get_custom_fonts( $request ) );
|
|
}
|
|
|
|
// Filter before doing any sorting.
|
|
if ( isset( $registered['include'], $request['include'] ) && ! empty( $request['include'] ) ) {
|
|
/**
|
|
* Include list.
|
|
*
|
|
* @var array{string} $include_list
|
|
*/
|
|
$include_list = $request['include'];
|
|
$include_list = array_map( 'strtolower', $include_list );
|
|
|
|
$fonts = array_values(
|
|
array_filter(
|
|
$fonts,
|
|
/**
|
|
* Font data.
|
|
*
|
|
* @param array{family: string} $font
|
|
* @return bool
|
|
*/
|
|
static fn( array $font ): bool => \in_array( strtolower( $font['family'] ), $include_list, true )
|
|
)
|
|
);
|
|
}
|
|
|
|
if ( 'all' === $request['service'] ) {
|
|
// Since the built-in fonts and custom fonts both are already sorted,
|
|
// we only need to sort when including both.
|
|
usort(
|
|
$fonts,
|
|
/**
|
|
* Font A and Font B.
|
|
*
|
|
* @param Font $a
|
|
* @param Font $b
|
|
* @return int
|
|
*/
|
|
static fn( array $a, array $b ): int => strnatcasecmp( $a['family'], $b['family'] )
|
|
);
|
|
}
|
|
}
|
|
|
|
return rest_ensure_response( $fonts );
|
|
}
|
|
|
|
/**
|
|
* Checks if a given request has access to read posts.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param WP_REST_Request $request Full details about the request.
|
|
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
|
|
*/
|
|
public function get_items_permissions_check( $request ) {
|
|
$post_type = get_post_type_object( $this->post_type );
|
|
|
|
if (
|
|
! $post_type ||
|
|
! current_user_can( $post_type->cap->read_post ) // phpcs:ignore WordPress.WP.Capabilities.Undetermined
|
|
) {
|
|
return new \WP_Error(
|
|
'rest_forbidden',
|
|
__( 'Sorry, you are not allowed to list fonts.', 'web-stories' ),
|
|
[ 'status' => rest_authorization_required_code() ]
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Force-deletes a single font.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param WP_REST_Request $request Full details about the request.
|
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
|
*/
|
|
public function delete_item( $request ) {
|
|
$request->set_param( 'force', true );
|
|
|
|
return parent::delete_item( $request );
|
|
}
|
|
|
|
/**
|
|
* Prepares a single post output for response.
|
|
*
|
|
* @SuppressWarnings(PHPMD.NPathComplexity)
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param WP_Post $item Post object.
|
|
* @param WP_REST_Request $request Request object.
|
|
* @return WP_REST_Response Response object.
|
|
*/
|
|
public function prepare_item_for_response( $item, $request ): WP_REST_Response {
|
|
// Restores the more descriptive, specific name for use within this method.
|
|
$post = $item;
|
|
$GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
|
|
|
setup_postdata( $post );
|
|
|
|
$fields = $this->get_fields_for_response( $request );
|
|
|
|
$data = [];
|
|
|
|
if ( rest_is_field_included( 'id', $fields ) ) {
|
|
$data['id'] = $post->ID;
|
|
}
|
|
|
|
if ( rest_is_field_included( 'family', $fields ) ) {
|
|
$data['family'] = $post->post_title;
|
|
}
|
|
|
|
if ( rest_is_field_included( 'service', $fields ) ) {
|
|
$data['service'] = 'custom';
|
|
}
|
|
|
|
/**
|
|
* Font data.
|
|
*
|
|
* @var array<string, mixed>|null $font_data
|
|
*/
|
|
$font_data = json_decode( $post->post_content, true );
|
|
|
|
if ( $font_data ) {
|
|
foreach ( $font_data as $key => $value ) {
|
|
if ( rest_is_field_included( $key, $fields ) ) {
|
|
$data[ $key ] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request context.
|
|
*
|
|
* @var string $context
|
|
*/
|
|
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
|
$data = $this->add_additional_fields_to_object( $data, $request );
|
|
$data = $this->filter_response_by_context( $data, $context );
|
|
|
|
/**
|
|
* Response object.
|
|
*
|
|
* @var WP_REST_Response $response
|
|
*/
|
|
$response = rest_ensure_response( $data );
|
|
|
|
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
|
|
$links = $this->prepare_links( $post );
|
|
$response->add_links( $links );
|
|
|
|
if ( ! empty( $links['self']['href'] ) ) {
|
|
$actions = $this->get_available_actions( $post, $request );
|
|
|
|
$self = $links['self']['href'];
|
|
|
|
foreach ( $actions as $rel ) {
|
|
$response->add_link( $rel, $self );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the query params for the fonts collection.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @return array<string, array<string, mixed>> Collection parameters.
|
|
*/
|
|
public function get_collection_params(): array {
|
|
$query_params = parent::get_collection_params();
|
|
|
|
$query_params['context']['default'] = 'view';
|
|
|
|
$query_params['search'] = [
|
|
'description' => __( 'Limit results to those matching a string.', 'web-stories' ),
|
|
'type' => 'string',
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
'validate_callback' => 'rest_validate_request_arg',
|
|
];
|
|
|
|
$query_params['include'] = [
|
|
'description' => __( 'Limit result set to specific fonts.', 'web-stories' ),
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'string',
|
|
],
|
|
'default' => [],
|
|
];
|
|
|
|
$query_params['service'] = [
|
|
'description' => __( 'Filter fonts by service.', 'web-stories' ),
|
|
'type' => 'string',
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
'default' => 'all',
|
|
'enum' => [
|
|
'all',
|
|
'custom',
|
|
'builtin', // system + fonts.google.com.
|
|
],
|
|
];
|
|
|
|
/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
|
|
return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $this->post_type );
|
|
}
|
|
|
|
/**
|
|
* Retrieves the font's schema, conforming to JSON Schema.
|
|
*
|
|
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @return array Item schema data.
|
|
*
|
|
* @phpstan-return Schema
|
|
*/
|
|
public function get_item_schema(): array {
|
|
if ( $this->schema ) {
|
|
/**
|
|
* Schema.
|
|
*
|
|
* @phpstan-var Schema $schema
|
|
*/
|
|
$schema = $this->add_additional_fields_schema( $this->schema );
|
|
return $schema;
|
|
}
|
|
|
|
$schema = [
|
|
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
|
'title' => $this->post_type,
|
|
'type' => 'object',
|
|
// Base properties for every font.
|
|
'properties' => [
|
|
'family' => [
|
|
'description' => __( 'The font family', 'web-stories' ),
|
|
'type' => [ 'string', 'null' ],
|
|
'context' => [ 'view', 'edit', 'embed' ],
|
|
'required' => true,
|
|
],
|
|
'fallbacks' => [
|
|
'description' => __( 'Fallback fonts', 'web-stories' ),
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'string',
|
|
],
|
|
'context' => [ 'view', 'edit' ],
|
|
'required' => true,
|
|
],
|
|
'weights' => [
|
|
'description' => __( 'Font weights', 'web-stories' ),
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'integer',
|
|
'minimum' => 0,
|
|
'maximum' => 900,
|
|
],
|
|
'minimum' => 1,
|
|
'context' => [ 'view', 'edit' ],
|
|
'required' => true,
|
|
],
|
|
'styles' => [
|
|
'description' => __( 'Font styles', 'web-stories' ),
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'string',
|
|
],
|
|
'minimum' => 1,
|
|
'context' => [ 'view', 'edit' ],
|
|
'required' => true,
|
|
],
|
|
'variants' => [
|
|
'description' => __( 'Font variants', 'web-stories' ),
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'integer',
|
|
'minimum' => 0,
|
|
'maximum' => 900,
|
|
],
|
|
'minimum' => 2,
|
|
'maximum' => 2,
|
|
],
|
|
'context' => [ 'view', 'edit' ],
|
|
'required' => true,
|
|
],
|
|
'service' => [
|
|
'description' => __( 'Font service', 'web-stories' ),
|
|
'type' => 'string',
|
|
'context' => [ 'view', 'edit' ],
|
|
'readonly' => true,
|
|
],
|
|
'metrics' => [
|
|
'description' => __( 'Font metrics', 'web-stories' ),
|
|
'type' => 'object',
|
|
'context' => [ 'view', 'edit' ],
|
|
'required' => true,
|
|
],
|
|
'id' => [
|
|
'description' => __( 'Unique identifier for the font.', 'web-stories' ),
|
|
'type' => 'integer',
|
|
'context' => [ 'view', 'edit', 'embed' ],
|
|
'readonly' => true,
|
|
],
|
|
'url' => [
|
|
'description' => __( 'Font URL.', 'web-stories' ),
|
|
'type' => 'string',
|
|
'format' => 'uri',
|
|
'context' => [ 'view', 'edit', 'embed' ],
|
|
'required' => true,
|
|
],
|
|
],
|
|
];
|
|
|
|
$this->schema = $schema;
|
|
|
|
/**
|
|
* Schema.
|
|
*
|
|
* @phpstan-var Schema $schema
|
|
*/
|
|
$schema = $this->add_additional_fields_schema( $this->schema );
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of Google fonts.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @return array<int, mixed> List of Google fonts.
|
|
*
|
|
* @phpstan-return Font[]
|
|
*/
|
|
protected function get_builtin_fonts(): array {
|
|
$file = WEBSTORIES_PLUGIN_DIR_PATH . 'includes/data/fonts/fonts.json';
|
|
|
|
if ( ! is_readable( $file ) ) {
|
|
return [];
|
|
}
|
|
|
|
$content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
|
|
|
|
if ( ! $content ) {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* List of Google Fonts.
|
|
*
|
|
* @var array|null $fonts
|
|
* @phpstan-var Font[]|null $fonts
|
|
*/
|
|
$fonts = json_decode( $content, true );
|
|
|
|
if ( ! $fonts ) {
|
|
return [];
|
|
}
|
|
|
|
return $fonts;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of custom fonts.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param WP_REST_Request $request Full details about the request.
|
|
* @return array<int, mixed> List of custom fonts.
|
|
*
|
|
* @phpstan-return Font[]
|
|
*/
|
|
protected function get_custom_fonts( $request ): array {
|
|
// Retrieve the list of registered collection query parameters.
|
|
$registered = $this->get_collection_params();
|
|
$args = [
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
];
|
|
|
|
/*
|
|
* This array defines mappings between public API query parameters whose
|
|
* values are accepted as-passed, and their internal WP_Query parameter
|
|
* name equivalents (some are the same). Only values which are also
|
|
* present in $registered will be set.
|
|
*/
|
|
$parameter_mappings = [
|
|
'search' => 's',
|
|
];
|
|
|
|
/*
|
|
* For each known parameter which is both registered and present in the request,
|
|
* set the parameter's value on the query $args.
|
|
*/
|
|
foreach ( $parameter_mappings as $api_param => $wp_param ) {
|
|
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
|
|
$args[ $wp_param ] = $request[ $api_param ];
|
|
}
|
|
}
|
|
|
|
// Ensure our per_page parameter overrides any provided posts_per_page filter.
|
|
if ( isset( $registered['per_page'] ) ) {
|
|
$args['posts_per_page'] = $request['per_page'];
|
|
}
|
|
|
|
// Force search to be case-insensitive.
|
|
|
|
// Force the post_type argument, since it's not a user input variable.
|
|
$args['post_type'] = $this->post_type;
|
|
$query_args = $this->prepare_items_query( $args, $request );
|
|
|
|
$posts_query = new WP_Query();
|
|
$query_result = $posts_query->query( $query_args );
|
|
|
|
/**
|
|
* List of custom fonts.
|
|
*
|
|
* @var array $posts
|
|
* @phpstan-var Font[] $posts
|
|
*/
|
|
$posts = [];
|
|
|
|
/**
|
|
* We're expecting a post object.
|
|
*
|
|
* @var WP_Post $post
|
|
*/
|
|
foreach ( $query_result as $post ) {
|
|
if ( ! $this->check_read_permission( $post ) ) {
|
|
continue;
|
|
}
|
|
|
|
$data = $this->prepare_item_for_response( $post, $request );
|
|
$posts[] = $this->prepare_response_for_collection( $data );
|
|
}
|
|
|
|
// Reset filter.
|
|
if ( 'edit' === $request['context'] ) {
|
|
remove_filter( 'post_password_required', [ $this, 'check_password_required' ] );
|
|
}
|
|
|
|
return $posts;
|
|
}
|
|
|
|
/**
|
|
* Prepares a single post for create.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param WP_REST_Request $request Request object.
|
|
* @return stdClass|WP_Error Post object or WP_Error.
|
|
*/
|
|
protected function prepare_item_for_database( $request ) {
|
|
$prepared_post = new stdClass();
|
|
$prepared_post->post_status = 'publish';
|
|
|
|
$font_data = [];
|
|
|
|
$fields = [
|
|
'family',
|
|
'fallbacks',
|
|
'weights',
|
|
'styles',
|
|
'variants',
|
|
'metrics',
|
|
'url',
|
|
];
|
|
|
|
$schema = $this->get_item_schema();
|
|
|
|
foreach ( $fields as $field ) {
|
|
if ( ! empty( $schema['properties'][ $field ] ) && ! empty( $request[ $field ] ) ) {
|
|
$font_data[ $field ] = $request[ $field ];
|
|
|
|
if ( 'family' === $field ) {
|
|
/**
|
|
* Request data.
|
|
*
|
|
* @var array{family: string} $request
|
|
*/
|
|
$font_family = trim( $request['family'] );
|
|
|
|
$prepared_post->post_title = $font_family;
|
|
|
|
if ( $this->font_exists( $font_family ) ) {
|
|
return new \WP_Error(
|
|
'rest_invalid_field',
|
|
__( 'A font with this name already exists', 'web-stories' ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$prepared_post->post_content = wp_json_encode( $font_data );
|
|
|
|
return $prepared_post;
|
|
}
|
|
|
|
/**
|
|
* Determines whether a font with the same name already exists.
|
|
*
|
|
* Performs a case-insensitive comparison.
|
|
*
|
|
* @since 1.16.0
|
|
*
|
|
* @param string $font_family Font family.
|
|
* @return bool Whether a font with this exact name already exists.
|
|
*/
|
|
private function font_exists( string $font_family ): bool {
|
|
$request = new WP_REST_Request(
|
|
WP_REST_Server::READABLE,
|
|
$this->namespace .
|
|
'/' . $this->rest_base
|
|
);
|
|
$request->set_param( 'include', [ $font_family ] );
|
|
$request->set_param( 'service', 'all' );
|
|
|
|
/**
|
|
* Response object.
|
|
*
|
|
* @var WP_REST_Response $response
|
|
*/
|
|
$response = $this->get_items( $request );
|
|
|
|
return ! empty( $response->get_data() );
|
|
}
|
|
}
|