<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57548] trunk: REST API: Introduce the necessary endpoints for the font library.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57548">57548</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57548","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>youknowriad</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-02-07 09:18:38 +0000 (Wed, 07 Feb 2024)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>REST API: Introduce the necessary endpoints for the font library.
This commits add three endpoints to retrieve and manipulate fonts in WordPress.
This commit also means that we now have a fully functional Font Library in the site editor.
Props get_dave, youknowriad, mmaattiiaass, grantmkin, swissspidy, mcsf, jorbin, ocean90.
See <a href="https://core.trac.wordpress.org/ticket/59166">#59166</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesfontsclasswpfontlibraryphp">trunk/src/wp-includes/fonts/class-wp-font-library.php</a></li>
<li><a href="#trunksrcwpincludespostphp">trunk/src/wp-includes/post.php</a></li>
<li><a href="#trunksrcwpincludesrestapiphp">trunk/src/wp-includes/rest-api.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontLibrarygetFontCollectionphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestschemasetupphp">trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php</a></li>
<li><a href="#trunktestsqunitfixtureswpapigeneratedjs">trunk/tests/qunit/fixtures/wp-api-generated.js</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestfontcollectionscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestfontfacescontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestfontfamiliescontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpRestFontCollectionsControllerphp">trunk/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpRestFontFacesControllerphp">trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpRestFontFamiliesControllerphp">trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesfontsclasswpfontlibraryphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/fonts/class-wp-font-library.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/fonts/class-wp-font-library.php 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/src/wp-includes/fonts/class-wp-font-library.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -115,14 +115,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.5.0
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param string $slug Font collection slug.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @return WP_Font_Collection|WP_Error Font collection object,
- * or WP_Error object if the font collection doesn't exist.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @return WP_Font_Collection|null Font collection object, or null if the font collection doesn't exist.
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public function get_font_collection( $slug ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $this->is_collection_registered( $slug ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> return $this->collections[ $slug ];
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- return new WP_Error( 'font_collection_not_found', __( 'Font collection not found.' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return null;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre></div>
<a id="trunksrcwpincludespostphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/post.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/post.php 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/src/wp-includes/post.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -567,14 +567,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> register_post_type(
</span><span class="cx" style="display: block; padding: 0 10px"> 'wp_font_family',
</span><span class="cx" style="display: block; padding: 0 10px"> array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'labels' => array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'labels' => array(
</ins><span class="cx" style="display: block; padding: 0 10px"> 'name' => __( 'Font Families' ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'singular_name' => __( 'Font Family' ),
</span><span class="cx" style="display: block; padding: 0 10px"> ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'public' => false,
- '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
- 'hierarchical' => false,
- 'capabilities' => array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'public' => false,
+ '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+ 'hierarchical' => false,
+ 'capabilities' => array(
</ins><span class="cx" style="display: block; padding: 0 10px"> 'read' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> 'read_private_posts' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> 'create_posts' => 'edit_theme_options',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -586,10 +586,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'delete_others_posts' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> 'delete_published_posts' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'map_meta_cap' => true,
- 'query_var' => false,
- 'show_in_rest' => false,
- 'rewrite' => false,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'map_meta_cap' => true,
+ 'query_var' => false,
+ 'rewrite' => false,
+ 'show_in_rest' => true,
+ 'rest_base' => 'font-families',
+ 'rest_controller_class' => 'WP_REST_Font_Families_Controller',
+ // Disable autosave endpoints for font families.
+ 'autosave_rest_controller_class' => 'stdClass',
</ins><span class="cx" style="display: block; padding: 0 10px"> )
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -596,14 +600,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> register_post_type(
</span><span class="cx" style="display: block; padding: 0 10px"> 'wp_font_face',
</span><span class="cx" style="display: block; padding: 0 10px"> array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'labels' => array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'labels' => array(
</ins><span class="cx" style="display: block; padding: 0 10px"> 'name' => __( 'Font Faces' ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'singular_name' => __( 'Font Face' ),
</span><span class="cx" style="display: block; padding: 0 10px"> ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'public' => false,
- '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
- 'hierarchical' => false,
- 'capabilities' => array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'public' => false,
+ '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+ 'hierarchical' => false,
+ 'capabilities' => array(
</ins><span class="cx" style="display: block; padding: 0 10px"> 'read' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> 'read_private_posts' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> 'create_posts' => 'edit_theme_options',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -615,10 +619,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'delete_others_posts' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> 'delete_published_posts' => 'edit_theme_options',
</span><span class="cx" style="display: block; padding: 0 10px"> ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'map_meta_cap' => true,
- 'query_var' => false,
- 'show_in_rest' => false,
- 'rewrite' => false,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'map_meta_cap' => true,
+ 'query_var' => false,
+ 'rewrite' => false,
+ 'show_in_rest' => true,
+ 'rest_base' => 'font-families/(?P<font_family_id>[\d]+)/font-faces',
+ 'rest_controller_class' => 'WP_REST_Font_Faces_Controller',
+ // Disable autosave endpoints for font faces.
+ 'autosave_rest_controller_class' => 'stdClass',
</ins><span class="cx" style="display: block; padding: 0 10px"> )
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestfontcollectionscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,322 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Rest Font Collections Controller.
+ *
+ * This file contains the class for the REST API Font Collections Controller.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.5.0
+ */
+
+/**
+ * Font Library Controller class.
+ *
+ * @since 6.5.0
+ */
+class WP_REST_Font_Collections_Controller extends WP_REST_Controller {
+
+ /**
+ * Constructor.
+ *
+ * @since 6.5.0
+ */
+ public function __construct() {
+ $this->rest_base = 'font-collections';
+ $this->namespace = 'wp/v2';
+ }
+
+ /**
+ * Registers the routes for the objects of the controller.
+ *
+ * @since 6.5.0
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => $this->get_collection_params(),
+
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P<slug>[\/\w-]+)',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Gets the font collections available.
+ *
+ * @since 6.5.0
+ *
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_items( $request ) {
+ $collections_all = WP_Font_Library::get_instance()->get_font_collections();
+
+ $page = $request['page'];
+ $per_page = $request['per_page'];
+ $total_items = count( $collections_all );
+ $max_pages = ceil( $total_items / $per_page );
+
+ if ( $page > $max_pages && $total_items > 0 ) {
+ return new WP_Error(
+ 'rest_post_invalid_page_number',
+ __( 'The page number requested is larger than the number of pages available.' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $collections_page = array_slice( $collections_all, ( $page - 1 ) * $per_page, $per_page );
+
+ $items = array();
+ foreach ( $collections_page as $collection ) {
+ $item = $this->prepare_item_for_response( $collection, $request );
+
+ // If there's an error loading a collection, skip it and continue loading valid collections.
+ if ( is_wp_error( $item ) ) {
+ continue;
+ }
+ $item = $this->prepare_response_for_collection( $item );
+ $items[] = $item;
+ }
+
+ $response = rest_ensure_response( $items );
+
+ $response->header( 'X-WP-Total', (int) $total_items );
+ $response->header( 'X-WP-TotalPages', (int) $max_pages );
+
+ $request_params = $request->get_query_params();
+ $collection_url = rest_url( $this->namespace . '/' . $this->rest_base );
+ $base = add_query_arg( urlencode_deep( $request_params ), $collection_url );
+
+ if ( $page > 1 ) {
+ $prev_page = $page - 1;
+
+ if ( $prev_page > $max_pages ) {
+ $prev_page = $max_pages;
+ }
+
+ $prev_link = add_query_arg( 'page', $prev_page, $base );
+ $response->link_header( 'prev', $prev_link );
+ }
+ if ( $max_pages > $page ) {
+ $next_page = $page + 1;
+ $next_link = add_query_arg( 'page', $next_page, $base );
+
+ $response->link_header( 'next', $next_link );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Gets a font collection.
+ *
+ * @since 6.5.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_item( $request ) {
+ $slug = $request->get_param( 'slug' );
+ $collection = WP_Font_Library::get_instance()->get_font_collection( $slug );
+
+ if ( ! $collection ) {
+ return new WP_Error( 'rest_font_collection_not_found', __( 'Font collection not found.' ), array( 'status' => 404 ) );
+ }
+
+ return $this->prepare_item_for_response( $collection, $request );
+ }
+
+ /**
+ * Prepare a single collection output for response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Font_Collection $item Font collection object.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function prepare_item_for_response( $item, $request ) {
+ $fields = $this->get_fields_for_response( $request );
+ $data = array();
+
+ if ( rest_is_field_included( 'slug', $fields ) ) {
+ $data['slug'] = $item->slug;
+ }
+
+ // If any data fields are requested, get the collection data.
+ $data_fields = array( 'name', 'description', 'font_families', 'categories' );
+ if ( ! empty( array_intersect( $fields, $data_fields ) ) ) {
+ $collection_data = $item->get_data();
+ if ( is_wp_error( $collection_data ) ) {
+ $collection_data->add_data( array( 'status' => 500 ) );
+ return $collection_data;
+ }
+
+ foreach ( $data_fields as $field ) {
+ if ( rest_is_field_included( $field, $fields ) ) {
+ $data[ $field ] = $collection_data[ $field ];
+ }
+ }
+ }
+
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) ) {
+ $links = $this->prepare_links( $item );
+ $response->add_links( $links );
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $response->data = $this->add_additional_fields_to_object( $response->data, $request );
+ $response->data = $this->filter_response_by_context( $response->data, $context );
+
+ /**
+ * Filters the font collection data for a REST API response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WP_Font_Collection $item The font collection object.
+ * @param WP_REST_Request $request Request used to generate the response.
+ */
+ return apply_filters( 'rest_prepare_font_collection', $response, $item, $request );
+ }
+
+ /**
+ * Retrieves the font collection's schema, conforming to JSON Schema.
+ *
+ * @since 6.5.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'font-collection',
+ 'type' => 'object',
+ 'properties' => array(
+ 'slug' => array(
+ 'description' => __( 'Unique identifier for the font collection.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'name' => array(
+ 'description' => __( 'The name for the font collection.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'description' => array(
+ 'description' => __( 'The description for the font collection.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'font_families' => array(
+ 'description' => __( 'The font families for the font collection.' ),
+ 'type' => 'array',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'categories' => array(
+ 'description' => __( 'The categories for the font collection.' ),
+ 'type' => 'array',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ /**
+ * Prepares links for the request.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Font_Collection $collection Font collection data
+ * @return array Links for the given font collection.
+ */
+ protected function prepare_links( $collection ) {
+ return array(
+ 'self' => array(
+ 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $collection->slug ) ),
+ ),
+ 'collection' => array(
+ 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
+ ),
+ );
+ }
+
+ /**
+ * Retrieves the search params for the font collections.
+ *
+ * @since 6.5.0
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+
+ $query_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
+
+ unset( $query_params['search'] );
+
+ /**
+ * Filters REST API collection parameters for the font collections controller.
+ *
+ * @since 6.5.0
+ *
+ * @param array $query_params JSON Schema-formatted collection parameters.
+ */
+ return apply_filters( 'rest_font_collections_collection_params', $query_params );
+ }
+
+ /**
+ * Checks whether the user has permissions to use the Fonts Collections.
+ *
+ * @since 6.5.0
+ *
+ * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
+ */
+ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access font collections.' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesrestapiendpointsclasswprestfontfacescontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,950 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * REST API: WP_REST_Font_Faces_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.5.0
+ */
+
+/**
+ * Class to access font faces through the REST API.
+ */
+class WP_REST_Font_Faces_Controller extends WP_REST_Posts_Controller {
+
+ /**
+ * The latest version of theme.json schema supported by the controller.
+ *
+ * @since 6.5.0
+ * @var int
+ */
+ const LATEST_THEME_JSON_VERSION_SUPPORTED = 2;
+
+ /**
+ * Whether the controller supports batching.
+ *
+ * @since 6.5.0
+ * @var false
+ */
+ protected $allow_batch = false;
+
+ /**
+ * Registers the routes for posts.
+ *
+ * @since 6.5.0
+ *
+ * @see register_rest_route()
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ 'args' => array(
+ 'font_family_id' => array(
+ 'description' => __( 'The ID for the parent font family of the font face.' ),
+ 'type' => 'integer',
+ 'required' => true,
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => $this->get_collection_params(),
+ ),
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_item' ),
+ 'permission_callback' => array( $this, 'create_item_permissions_check' ),
+ 'args' => $this->get_create_params(),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P<id>[\d]+)',
+ array(
+ 'args' => array(
+ 'font_family_id' => array(
+ 'description' => __( 'The ID for the parent font family of the font face.' ),
+ 'type' => 'integer',
+ 'required' => true,
+ ),
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the font face.' ),
+ 'type' => 'integer',
+ 'required' => true,
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( $this, 'delete_item' ),
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+ 'args' => array(
+ 'force' => array(
+ 'type' => 'boolean',
+ 'default' => false,
+ 'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ),
+ ),
+ ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Checks if a given request has access to font faces.
+ *
+ * @since 6.5.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 ( ! current_user_can( $post_type->cap->read ) ) {
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access font faces.' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a given request has access to a font face.
+ *
+ * @since 6.5.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_item_permissions_check( $request ) {
+ $post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access this font face.' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates settings when creating a font face.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font face settings.
+ * @param WP_REST_Request $request Request object.
+ * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object.
+ */
+ public function validate_create_font_face_settings( $value, $request ) {
+ $settings = json_decode( $value, true );
+
+ // Check settings string is valid JSON.
+ if ( null === $settings ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'font_face_settings parameter must be a valid JSON string.' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Check that the font face settings match the theme.json schema.
+ $schema = $this->get_item_schema()['properties']['font_face_settings'];
+ $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' );
+
+ if ( is_wp_error( $has_valid_settings ) ) {
+ $has_valid_settings->add_data( array( 'status' => 400 ) );
+ return $has_valid_settings;
+ }
+
+ // Check that none of the required settings are empty values.
+ $required = $schema['required'];
+ foreach ( $required as $key ) {
+ if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Name of the missing font face settings parameter, e.g. "font_face_settings[src]". */
+ sprintf( __( '%s cannot be empty.' ), "font_face_setting[ $key ]" ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] );
+ $files = $request->get_file_params();
+
+ foreach ( $srcs as $src ) {
+ // Check that each src is a non-empty string.
+ $src = ltrim( $src );
+ if ( empty( $src ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Font face source parameter name: "font_face_settings[src]". */
+ sprintf( __( '%s values must be non-empty strings.' ), 'font_face_settings[src]' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Check that srcs are valid URLs or file references.
+ if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: 1: Font face source parameter name: "font_face_settings[src]", 2: The invalid src value. */
+ sprintf( __( '%1$s value "%2$s" must be a valid URL or file reference.' ), 'font_face_settings[src]', $src ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ // Check that each file in the request references a src in the settings.
+ foreach ( array_keys( $files ) as $file ) {
+ if ( ! in_array( $file, $srcs, true ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: 1: File key (e.g. "file-0") in the request data, 2: Font face source parameter name: "font_face_settings[src]". */
+ sprintf( __( 'File %1$s must be used in %2$s.' ), $file, 'font_face_settings[src]' ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Sanitizes the font face settings when creating a font face.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font face settings.
+ * @return array Decoded and sanitized array of font face settings.
+ */
+ public function sanitize_font_face_settings( $value ) {
+ // Settings arrive as stringified JSON, since this is a multipart/form-data request.
+ $settings = json_decode( $value, true );
+ $schema = $this->get_item_schema()['properties']['font_face_settings']['properties'];
+
+ // Sanitize settings based on callbacks in the schema.
+ foreach ( $settings as $key => $value ) {
+ $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
+ $settings[ $key ] = call_user_func( $sanitize_callback, $value );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Retrieves a collection of font faces within the parent font family.
+ *
+ * @since 6.5.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 ) {
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ return parent::get_items( $request );
+ }
+
+ /**
+ * Retrieves a single font face within the parent font family.
+ *
+ * @since 6.5.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_item( $request ) {
+ $post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ // Check that the font face has a valid parent font family.
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ if ( (int) $font_family->ID !== (int) $post->post_parent ) {
+ return new WP_Error(
+ 'rest_font_face_parent_id_mismatch',
+ /* translators: %d: A post id. */
+ sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ),
+ array( 'status' => 404 )
+ );
+ }
+
+ return parent::get_item( $request );
+ }
+
+ /**
+ * Creates a font face for the parent font family.
+ *
+ * @since 6.5.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 create_item( $request ) {
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ // Settings have already been decoded by ::sanitize_font_face_settings().
+ $settings = $request->get_param( 'font_face_settings' );
+ $file_params = $request->get_file_params();
+
+ // Check that the necessary font face properties are unique.
+ $query = new WP_Query(
+ array(
+ 'post_type' => $this->post_type,
+ 'posts_per_page' => 1,
+ 'title' => WP_Font_Utils::get_font_face_slug( $settings ),
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+ if ( ! empty( $query->posts ) ) {
+ return new WP_Error(
+ 'rest_duplicate_font_face',
+ __( 'A font face matching those settings already exists.' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Move the uploaded font asset from the temp folder to the fonts directory.
+ if ( ! function_exists( 'wp_handle_upload' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ }
+
+ $srcs = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src'];
+ $processed_srcs = array();
+ $font_file_meta = array();
+
+ foreach ( $srcs as $src ) {
+ // If src not a file reference, use it as is.
+ if ( ! isset( $file_params[ $src ] ) ) {
+ $processed_srcs[] = $src;
+ continue;
+ }
+
+ $file = $file_params[ $src ];
+ $font_file = $this->handle_font_file_upload( $file );
+ if ( is_wp_error( $font_file ) ) {
+ return $font_file;
+ }
+
+ $processed_srcs[] = $font_file['url'];
+ $font_file_meta[] = $this->relative_fonts_path( $font_file['file'] );
+ }
+
+ // Store the updated settings for prepare_item_for_database to use.
+ $settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs;
+ $request->set_param( 'font_face_settings', $settings );
+
+ // Ensure that $settings data is slashed, so values with quotes are escaped.
+ // WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content.
+ $font_face_post = parent::create_item( $request );
+
+ if ( is_wp_error( $font_face_post ) ) {
+ return $font_face_post;
+ }
+
+ $font_face_id = $font_face_post->data['id'];
+
+ foreach ( $font_file_meta as $font_file_path ) {
+ add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path );
+ }
+
+ return $font_face_post;
+ }
+
+ /**
+ * Deletes a single font face.
+ *
+ * @since 6.5.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 ) {
+ $post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ $font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
+ if ( is_wp_error( $font_family ) ) {
+ return $font_family;
+ }
+
+ if ( (int) $font_family->ID !== (int) $post->post_parent ) {
+ return new WP_Error(
+ 'rest_font_face_parent_id_mismatch',
+ /* translators: %d: A post id. */
+ sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+ // We don't support trashing for font faces.
+ if ( ! $force ) {
+ return new WP_Error(
+ 'rest_trash_not_supported',
+ /* translators: %s: force=true */
+ sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ),
+ array( 'status' => 501 )
+ );
+ }
+
+ return parent::delete_item( $request );
+ }
+
+ /**
+ * Prepares a single font face output for response.
+ *
+ * @since 6.5.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 ) {
+ $fields = $this->get_fields_for_response( $request );
+ $data = array();
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = $item->ID;
+ }
+ if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
+ $data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED;
+ }
+
+ if ( rest_is_field_included( 'parent', $fields ) ) {
+ $data['parent'] = $item->post_parent;
+ }
+
+ if ( rest_is_field_included( 'font_face_settings', $fields ) ) {
+ $data['font_face_settings'] = $this->get_settings_from_post( $item );
+ }
+
+ $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 = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = $this->prepare_links( $item );
+ $response->add_links( $links );
+ }
+
+ /**
+ * Filters the font face data for a REST API response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WP_Post $post Font face post object.
+ * @param WP_REST_Request $request Request object.
+ */
+ return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request );
+ }
+
+ /**
+ * Retrieves the post's schema, conforming to JSON Schema.
+ *
+ * @since 6.5.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ // Base properties for every Post.
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the post.', 'default' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'theme_json_version' => array(
+ 'description' => __( 'Version of the theme.json schema used for the typography settings.' ),
+ 'type' => 'integer',
+ 'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
+ 'minimum' => 2,
+ 'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'parent' => array(
+ 'description' => __( 'The ID for the parent font family of the font face.' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ // Font face settings come directly from theme.json schema
+ // See https://schemas.wp.org/trunk/theme.json
+ 'font_face_settings' => array(
+ 'description' => __( 'font-face declaration in theme.json format.' ),
+ 'type' => 'object',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'properties' => array(
+ 'fontFamily' => array(
+ 'description' => __( 'CSS font-family value.' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'arg_options' => array(
+ 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
+ ),
+ ),
+ 'fontStyle' => array(
+ 'description' => __( 'CSS font-style value.' ),
+ 'type' => 'string',
+ 'default' => 'normal',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'fontWeight' => array(
+ 'description' => __( 'List of available font weights, separated by a space.' ),
+ 'default' => '400',
+ // Changed from `oneOf` to avoid errors from loose type checking.
+ // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check.
+ 'type' => array( 'string', 'integer' ),
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'fontDisplay' => array(
+ 'description' => __( 'CSS font-display value.' ),
+ 'type' => 'string',
+ 'default' => 'fallback',
+ 'enum' => array(
+ 'auto',
+ 'block',
+ 'fallback',
+ 'swap',
+ 'optional',
+ ),
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'src' => array(
+ 'description' => __( 'Paths or URLs to the font files.' ),
+ // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array,
+ // and causing a "matches more than one of the expected formats" error.
+ 'anyOf' => array(
+ array(
+ 'type' => 'string',
+ ),
+ array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ ),
+ ),
+ 'default' => array(),
+ 'arg_options' => array(
+ 'sanitize_callback' => function ( $value ) {
+ return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value );
+ },
+ ),
+ ),
+ 'fontStretch' => array(
+ 'description' => __( 'CSS font-stretch value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'ascentOverride' => array(
+ 'description' => __( 'CSS ascent-override value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'descentOverride' => array(
+ 'description' => __( 'CSS descent-override value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'fontVariant' => array(
+ 'description' => __( 'CSS font-variant value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'fontFeatureSettings' => array(
+ 'description' => __( 'CSS font-feature-settings value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'fontVariationSettings' => array(
+ 'description' => __( 'CSS font-variation-settings value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'lineGapOverride' => array(
+ 'description' => __( 'CSS line-gap-override value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'sizeAdjust' => array(
+ 'description' => __( 'CSS size-adjust value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'unicodeRange' => array(
+ 'description' => __( 'CSS unicode-range value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'preview' => array(
+ 'description' => __( 'URL to a preview image of the font face.' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'default' => '',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_url',
+ ),
+ ),
+ ),
+ 'required' => array( 'fontFamily', 'src' ),
+ 'additionalProperties' => false,
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ /**
+ * Retrieves the item's schema for display / public consumption purposes.
+ *
+ * @since 6.5.0
+ *
+ * @return array Public item schema data.
+ */
+ public function get_public_item_schema() {
+
+ $schema = parent::get_public_item_schema();
+
+ // Also remove `arg_options' from child font_family_settings properties, since the parent
+ // controller only handles the top level properties.
+ foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) {
+ unset( $property['arg_options'] );
+ }
+
+ return $schema;
+ }
+
+ /**
+ * Retrieves the query params for the font face collection.
+ *
+ * @since 6.5.0
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+
+ // Remove unneeded params.
+ unset(
+ $query_params['after'],
+ $query_params['modified_after'],
+ $query_params['before'],
+ $query_params['modified_before'],
+ $query_params['search'],
+ $query_params['search_columns'],
+ $query_params['slug'],
+ $query_params['status']
+ );
+
+ $query_params['orderby']['default'] = 'id';
+ $query_params['orderby']['enum'] = array( 'id', 'include' );
+
+ /**
+ * Filters collection parameters for the font face controller.
+ *
+ * @since 6.5.0
+ *
+ * @param array $query_params JSON Schema-formatted collection parameters.
+ */
+ return apply_filters( 'rest_wp_font_face_collection_params', $query_params );
+ }
+
+ /**
+ * Get the params used when creating a new font face.
+ *
+ * @since 6.5.0
+ *
+ * @return array Font face create arguments.
+ */
+ public function get_create_params() {
+ $properties = $this->get_item_schema()['properties'];
+ return array(
+ 'theme_json_version' => $properties['theme_json_version'],
+ // When creating, font_face_settings is stringified JSON, to work with multipart/form-data used
+ // when uploading font files.
+ 'font_face_settings' => array(
+ 'description' => __( 'font-face declaration in theme.json format, encoded as a string.' ),
+ 'type' => 'string',
+ 'required' => true,
+ 'validate_callback' => array( $this, 'validate_create_font_face_settings' ),
+ 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ),
+ ),
+ );
+ }
+
+ /**
+ * Get the parent font family, if the ID is valid.
+ *
+ * @since 6.5.0
+ *
+ * @param int $font_family_id Supplied ID.
+ * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
+ */
+ protected function get_parent_font_family_post( $font_family_id ) {
+ $error = new WP_Error(
+ 'rest_post_invalid_parent',
+ __( 'Invalid post parent ID.', 'default' ),
+ array( 'status' => 404 )
+ );
+
+ if ( (int) $font_family_id <= 0 ) {
+ return $error;
+ }
+
+ $font_family_post = get_post( (int) $font_family_id );
+
+ if ( empty( $font_family_post ) || empty( $font_family_post->ID )
+ || 'wp_font_family' !== $font_family_post->post_type
+ ) {
+ return $error;
+ }
+
+ return $font_family_post;
+ }
+
+ /**
+ * Prepares links for the request.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Post object.
+ * @return array Links for the given post.
+ */
+ protected function prepare_links( $post ) {
+ // Entity meta.
+ return array(
+ 'self' => array(
+ 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ),
+ ),
+ 'collection' => array(
+ 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ),
+ ),
+ 'parent' => array(
+ 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ),
+ ),
+ );
+ }
+
+ /**
+ * Prepares a single font face post for creation.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return stdClass Post object.
+ */
+ protected function prepare_item_for_database( $request ) {
+ $prepared_post = new stdClass();
+
+ // Settings have already been decoded by ::sanitize_font_face_settings().
+ $settings = $request->get_param( 'font_face_settings' );
+
+ // Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting,
+ // which may contain multibyte characters.
+ $title = WP_Font_Utils::get_font_face_slug( $settings );
+
+ $prepared_post->post_type = $this->post_type;
+ $prepared_post->post_parent = $request['font_family_id'];
+ $prepared_post->post_status = 'publish';
+ $prepared_post->post_title = $title;
+ $prepared_post->post_name = sanitize_title( $title );
+ $prepared_post->post_content = wp_json_encode( $settings );
+
+ return $prepared_post;
+ }
+
+ /**
+ * Sanitizes a single src value for a font face.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Font face src that is a URL or the key for a $_FILES array item.
+ * @return string Sanitized value.
+ */
+ protected function sanitize_src( $value ) {
+ $value = ltrim( $value );
+ return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value );
+ }
+
+ /**
+ * Handles the upload of a font file using wp_handle_upload().
+ *
+ * @since 6.5.0
+ *
+ * @param array $file Single file item from $_FILES.
+ * @return array|WP_Error Array containing uploaded file attributes on success, or WP_Error object on failure.
+ */
+ protected function handle_font_file_upload( $file ) {
+ add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
+ add_filter( 'upload_dir', 'wp_get_font_dir' );
+
+ $overrides = array(
+ 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ),
+ // Arbitrary string to avoid the is_uploaded_file() check applied
+ // when using 'wp_handle_upload'.
+ 'action' => 'wp_handle_font_upload',
+ // Not testing a form submission.
+ 'test_form' => false,
+ // Seems mime type for files that are not images cannot be tested.
+ // See wp_check_filetype_and_ext().
+ 'test_type' => true,
+ // Only allow uploading font files for this request.
+ 'mimes' => WP_Font_Utils::get_allowed_font_mime_types(),
+ );
+
+ $uploaded_file = wp_handle_upload( $file, $overrides );
+
+ remove_filter( 'upload_dir', 'wp_get_font_dir' );
+ remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
+
+ return $uploaded_file;
+ }
+
+ /**
+ * Handles file upload error.
+ *
+ * @since 6.5.0
+ *
+ * @param array $file File upload data.
+ * @param string $message Error message from wp_handle_upload().
+ * @return WP_Error WP_Error object.
+ */
+ public function handle_font_file_upload_error( $file, $message ) {
+ $status = 500;
+ $code = 'rest_font_upload_unknown_error';
+
+ if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) {
+ $status = 400;
+ $code = 'rest_font_upload_invalid_file_type';
+ }
+
+ return new WP_Error( $code, $message, array( 'status' => $status ) );
+ }
+
+ /**
+ * Returns relative path to an uploaded font file.
+ *
+ * The path is relative to the current fonts directory.
+ *
+ * @since 6.5.0
+ * @access private
+ *
+ * @param string $path Full path to the file.
+ * @return string Relative path on success, unchanged path on failure.
+ */
+ protected function relative_fonts_path( $path ) {
+ $new_path = $path;
+
+ $fonts_dir = wp_get_font_dir();
+ if ( str_starts_with( $new_path, $fonts_dir['path'] ) ) {
+ $new_path = str_replace( $fonts_dir, '', $new_path );
+ $new_path = ltrim( $new_path, '/' );
+ }
+
+ return $new_path;
+ }
+
+ /**
+ * Gets the font face's settings from the post.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Font face post object.
+ * @return array Font face settings array.
+ */
+ protected function get_settings_from_post( $post ) {
+ $settings = json_decode( $post->post_content, true );
+ $properties = $this->get_item_schema()['properties']['font_face_settings']['properties'];
+
+ // Provide required, empty settings if needed.
+ if ( null === $settings ) {
+ $settings = array(
+ 'fontFamily' => '',
+ 'src' => array(),
+ );
+ }
+
+ // Only return the properties defined in the schema.
+ return array_intersect_key( $settings, $properties );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesrestapiendpointsclasswprestfontfamiliescontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,564 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * REST API: WP_REST_Font_Families_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.5.0
+ */
+
+/**
+ * Font Families Controller class.
+ *
+ * @since 6.5.0
+ */
+class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller {
+
+ /**
+ * The latest version of theme.json schema supported by the controller.
+ *
+ * @since 6.5.0
+ * @var int
+ */
+ const LATEST_THEME_JSON_VERSION_SUPPORTED = 2;
+
+ /**
+ * Whether the controller supports batching.
+ *
+ * @since 6.5.0
+ * @var false
+ */
+ protected $allow_batch = false;
+
+ /**
+ * Checks if a given request has access to font families.
+ *
+ * @since 6.5.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 ( ! current_user_can( $post_type->cap->read ) ) {
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access font families.' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a given request has access to a font family.
+ *
+ * @since 6.5.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_item_permissions_check( $request ) {
+ $post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
+ return new WP_Error(
+ 'rest_cannot_read',
+ __( 'Sorry, you are not allowed to access this font family.' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates settings when creating or updating a font family.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font family settings.
+ * @param WP_REST_Request $request Request object.
+ * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object.
+ */
+ public function validate_font_family_settings( $value, $request ) {
+ $settings = json_decode( $value, true );
+
+ // Check settings string is valid JSON.
+ if ( null === $settings ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Parameter name: "font_family_settings". */
+ sprintf( __( '%s parameter must be a valid JSON string.' ), 'font_family_settings' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $schema = $this->get_item_schema()['properties']['font_family_settings'];
+ $required = $schema['required'];
+
+ if ( isset( $request['id'] ) ) {
+ // Allow sending individual properties if we are updating an existing font family.
+ unset( $schema['required'] );
+
+ // But don't allow updating the slug, since it is used as a unique identifier.
+ if ( isset( $settings['slug'] ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Name of parameter being updated: font_family_settings[slug]". */
+ sprintf( __( '%s cannot be updated.' ), 'font_family_settings[slug]' ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ // Check that the font face settings match the theme.json schema.
+ $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' );
+
+ if ( is_wp_error( $has_valid_settings ) ) {
+ $has_valid_settings->add_data( array( 'status' => 400 ) );
+ return $has_valid_settings;
+ }
+
+ // Check that none of the required settings are empty values.
+ foreach ( $required as $key ) {
+ if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: Name of the empty font family setting parameter, e.g. "font_family_settings[slug]". */
+ sprintf( __( '%s cannot be empty.' ), "font_family_settings[ $key ]" ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Sanitizes the font family settings when creating or updating a font family.
+ *
+ * @since 6.5.0
+ *
+ * @param string $value Encoded JSON string of font family settings.
+ * @return array Decoded array of font family settings.
+ */
+ public function sanitize_font_family_settings( $value ) {
+ // Settings arrive as stringified JSON, since this is a multipart/form-data request.
+ $settings = json_decode( $value, true );
+ $schema = $this->get_item_schema()['properties']['font_family_settings']['properties'];
+
+ // Sanitize settings based on callbacks in the schema.
+ foreach ( $settings as $key => $value ) {
+ $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
+ $settings[ $key ] = call_user_func( $sanitize_callback, $value );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Creates a single font family.
+ *
+ * @since 6.5.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 create_item( $request ) {
+ $settings = $request->get_param( 'font_family_settings' );
+
+ // Check that the font family slug is unique.
+ $query = new WP_Query(
+ array(
+ 'post_type' => $this->post_type,
+ 'posts_per_page' => 1,
+ 'name' => $settings['slug'],
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+ if ( ! empty( $query->posts ) ) {
+ return new WP_Error(
+ 'rest_duplicate_font_family',
+ /* translators: %s: Font family slug. */
+ sprintf( __( 'A font family with slug "%s" already exists.' ), $settings['slug'] ),
+ array( 'status' => 400 )
+ );
+ }
+
+ return parent::create_item( $request );
+ }
+
+ /**
+ * Deletes a single font family.
+ *
+ * @since 6.5.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 ) {
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+ // We don't support trashing for font families.
+ if ( ! $force ) {
+ return new WP_Error(
+ 'rest_trash_not_supported',
+ /* translators: %s: force=true */
+ sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ),
+ array( 'status' => 501 )
+ );
+ }
+
+ return parent::delete_item( $request );
+ }
+
+ /**
+ * Prepares a single font family output for response.
+ *
+ * @since 6.5.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 ) {
+ $fields = $this->get_fields_for_response( $request );
+ $data = array();
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = $item->ID;
+ }
+
+ if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
+ $data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED;
+ }
+
+ if ( rest_is_field_included( 'font_faces', $fields ) ) {
+ $data['font_faces'] = $this->get_font_face_ids( $item->ID );
+ }
+
+ if ( rest_is_field_included( 'font_family_settings', $fields ) ) {
+ $data['font_family_settings'] = $this->get_settings_from_post( $item );
+ }
+
+ $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 = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) ) {
+ $links = $this->prepare_links( $item );
+ $response->add_links( $links );
+ }
+
+ /**
+ * Filters the font family data for a REST API response.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WP_Post $post Font family post object.
+ * @param WP_REST_Request $request Request object.
+ */
+ return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request );
+ }
+
+ /**
+ * Retrieves the post's schema, conforming to JSON Schema.
+ *
+ * @since 6.5.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ // Base properties for every Post.
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the post.', 'default' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'theme_json_version' => array(
+ 'description' => __( 'Version of the theme.json schema used for the typography settings.' ),
+ 'type' => 'integer',
+ 'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
+ 'minimum' => 2,
+ 'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'font_faces' => array(
+ 'description' => __( 'The IDs of the child font faces in the font family.' ),
+ 'type' => 'array',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+ ),
+ // Font family settings come directly from theme.json schema
+ // See https://schemas.wp.org/trunk/theme.json
+ 'font_family_settings' => array(
+ 'description' => __( 'font-face definition in theme.json format.' ),
+ 'type' => 'object',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'properties' => array(
+ 'name' => array(
+ 'description' => __( 'Name of the font family preset, translatable.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ 'slug' => array(
+ 'description' => __( 'Kebab-case unique identifier for the font family preset.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_title',
+ ),
+ ),
+ 'fontFamily' => array(
+ 'description' => __( 'CSS font-family value.' ),
+ 'type' => 'string',
+ 'arg_options' => array(
+ 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
+ ),
+ ),
+ 'preview' => array(
+ 'description' => __( 'URL to a preview image of the font family.' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'default' => '',
+ 'arg_options' => array(
+ 'sanitize_callback' => 'sanitize_url',
+ ),
+ ),
+ ),
+ 'required' => array( 'name', 'slug', 'fontFamily' ),
+ 'additionalProperties' => false,
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ /**
+ * Retrieves the item's schema for display / public consumption purposes.
+ *
+ * @since 6.5.0
+ *
+ * @return array Public item schema data.
+ */
+ public function get_public_item_schema() {
+
+ $schema = parent::get_public_item_schema();
+
+ // Also remove `arg_options' from child font_family_settings properties, since the parent
+ // controller only handles the top level properties.
+ foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) {
+ unset( $property['arg_options'] );
+ }
+
+ return $schema;
+ }
+
+ /**
+ * Retrieves the query params for the font family collection.
+ *
+ * @since 6.5.0
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+
+ // Remove unneeded params.
+ unset(
+ $query_params['after'],
+ $query_params['modified_after'],
+ $query_params['before'],
+ $query_params['modified_before'],
+ $query_params['search'],
+ $query_params['search_columns'],
+ $query_params['status']
+ );
+
+ $query_params['orderby']['default'] = 'id';
+ $query_params['orderby']['enum'] = array( 'id', 'include' );
+
+ /**
+ * Filters collection parameters for the font family controller.
+ *
+ * @since 6.5.0
+ *
+ * @param array $query_params JSON Schema-formatted collection parameters.
+ */
+ return apply_filters( 'rest_wp_font_family_collection_params', $query_params );
+ }
+
+ /**
+ * Get the arguments used when creating or updating a font family.
+ *
+ * @since 6.5.0
+ *
+ * @return array Font family create/edit arguments.
+ */
+ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
+ if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) {
+ $properties = $this->get_item_schema()['properties'];
+ return array(
+ 'theme_json_version' => $properties['theme_json_version'],
+ // When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data.
+ // Font families don't currently support file uploads, but may accept preview files in the future.
+ 'font_family_settings' => array(
+ 'description' => __( 'font-family declaration in theme.json format, encoded as a string.' ),
+ 'type' => 'string',
+ 'required' => true,
+ 'validate_callback' => array( $this, 'validate_font_family_settings' ),
+ 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ),
+ ),
+ );
+ }
+
+ return parent::get_endpoint_args_for_item_schema( $method );
+ }
+
+ /**
+ * Get the child font face post IDs.
+ *
+ * @since 6.5.0
+ *
+ * @param int $font_family_id Font family post ID.
+ * @return int[] Array of child font face post IDs.
+ */
+ protected function get_font_face_ids( $font_family_id ) {
+ $query = new WP_Query(
+ array(
+ 'fields' => 'ids',
+ 'post_parent' => $font_family_id,
+ 'post_type' => 'wp_font_face',
+ 'posts_per_page' => 99,
+ 'order' => 'ASC',
+ 'orderby' => 'id',
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+
+ return $query->posts;
+ }
+
+ /**
+ * Prepares font family links for the request.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Post object.
+ * @return array Links for the given post.
+ */
+ protected function prepare_links( $post ) {
+ // Entity meta.
+ $links = parent::prepare_links( $post );
+
+ return array(
+ 'self' => $links['self'],
+ 'collection' => $links['collection'],
+ 'font_faces' => $this->prepare_font_face_links( $post->ID ),
+ );
+ }
+
+ /**
+ * Prepares child font face links for the request.
+ *
+ * @param int $font_family_id Font family post ID.
+ * @return array Links for the child font face posts.
+ */
+ protected function prepare_font_face_links( $font_family_id ) {
+ $font_face_ids = $this->get_font_face_ids( $font_family_id );
+ $links = array();
+ foreach ( $font_face_ids as $font_face_id ) {
+ $links[] = array(
+ 'embeddable' => true,
+ 'href' => rest_url( sprintf( '%s/%s/%s/font-faces/%s', $this->namespace, $this->rest_base, $font_family_id, $font_face_id ) ),
+ );
+ }
+ return $links;
+ }
+
+ /**
+ * Prepares a single font family post for create or update.
+ *
+ * @since 6.5.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();
+ // Settings have already been decoded by ::sanitize_font_family_settings().
+ $settings = $request->get_param( 'font_family_settings' );
+
+ // This is an update and we merge with the existing font family.
+ if ( isset( $request['id'] ) ) {
+ $existing_post = $this->get_post( $request['id'] );
+ if ( is_wp_error( $existing_post ) ) {
+ return $existing_post;
+ }
+
+ $prepared_post->ID = $existing_post->ID;
+ $existing_settings = $this->get_settings_from_post( $existing_post );
+ $settings = array_merge( $existing_settings, $settings );
+ }
+
+ $prepared_post->post_type = $this->post_type;
+ $prepared_post->post_status = 'publish';
+ $prepared_post->post_title = $settings['name'];
+ $prepared_post->post_name = sanitize_title( $settings['slug'] );
+
+ // Remove duplicate information from settings.
+ unset( $settings['name'] );
+ unset( $settings['slug'] );
+
+ $prepared_post->post_content = wp_json_encode( $settings );
+
+ return $prepared_post;
+ }
+
+ /**
+ * Gets the font family's settings from the post.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Post $post Font family post object.
+ * @return array Font family settings array.
+ */
+ protected function get_settings_from_post( $post ) {
+ $settings_json = json_decode( $post->post_content, true );
+
+ // Default to empty strings if the settings are missing.
+ return array(
+ 'name' => isset( $post->post_title ) && $post->post_title ? $post->post_title : '',
+ 'slug' => isset( $post->post_name ) && $post->post_name ? $post->post_name : '',
+ 'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '',
+ 'preview' => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '',
+ );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesrestapiphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api.php 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/src/wp-includes/rest-api.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -391,6 +391,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> // Navigation Fallback.
</span><span class="cx" style="display: block; padding: 0 10px"> $controller = new WP_REST_Navigation_Fallback_Controller();
</span><span class="cx" style="display: block; padding: 0 10px"> $controller->register_routes();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ // Font Collections.
+ $font_collections_controller = new WP_REST_Font_Collections_Controller();
+ $font_collections_controller->register_routes();
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/src/wp-settings.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -310,6 +310,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-templates-controller.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-url-details-controller.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-families-controller.php';
+require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-faces-controller.php';
+require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-collections-controller.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php';
</span></span></pre></div>
<a id="trunktestsphpunittestsfontsfontlibrarywpFontLibrarygetFontCollectionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -25,6 +25,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> public function test_should_get_no_font_collection_if_the_slug_is_not_registered() {
</span><span class="cx" style="display: block; padding: 0 10px"> $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'not-registered-font-collection' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->assertWPError( $font_collection );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->assertNull( $font_collection );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestsfontsfontlibrarywpRestFontCollectionsControllerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,209 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Font_Collections_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.5.0
+ *
+ * @group restapi
+ * @group fonts
+ * @group font-library
+ *
+ * @coversDefaultClass WP_REST_Font_Collections_Controller
+ */
+class Tests_REST_WpRestFontCollectionsController extends WP_Test_REST_Controller_Testcase {
+ protected static $admin_id;
+ protected static $editor_id;
+ protected static $mock_file;
+
+
+ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
+ // Clear the font collections.
+ $collections = WP_Font_Library::get_instance()->get_font_collections();
+ foreach ( $collections as $slug => $collection ) {
+ WP_Font_Library::get_instance()->unregister_font_collection( $slug );
+ }
+
+ self::$admin_id = $factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ self::$editor_id = $factory->user->create(
+ array(
+ 'role' => 'editor',
+ )
+ );
+ $mock_file = wp_tempnam( 'my-collection-data-' );
+ file_put_contents( $mock_file, '{"name": "Mock Collection", "font_families": [ "mock" ], "categories": [ "mock" ] }' );
+
+ wp_register_font_collection( 'mock-col-slug', $mock_file );
+ }
+
+ public static function wpTearDownAfterClass() {
+ self::delete_user( self::$admin_id );
+ self::delete_user( self::$editor_id );
+ wp_unregister_font_collection( 'mock-col-slug' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::register_routes
+ */
+ public function test_register_routes() {
+ $routes = rest_get_server()->get_routes();
+ $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'Rest server has not the collections path initialized.' );
+ $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P<slug>[\/\w-]+)'], 'Rest server has not the collection path initialized.' );
+
+ $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections initialized.' );
+ $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P<slug>[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection initialized.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::get_items
+ */
+ public function test_get_items() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' );
+ $response = rest_get_server()->dispatch( $request );
+ $content = $response->get_data();
+ $this->assertIsArray( $content );
+ $this->assertSame( 200, $response->get_status() );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::get_items
+ */
+ public function test_get_items_should_only_return_valid_collections() {
+ $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' );
+
+ wp_set_current_user( self::$admin_id );
+ wp_register_font_collection( 'invalid-collection', 'invalid-collection-file' );
+
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' );
+ $response = rest_get_server()->dispatch( $request );
+ $content = $response->get_data();
+
+ wp_unregister_font_collection( 'invalid-collection' );
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertCount( 1, $content, 'The response should only contain valid collections.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::get_item
+ */
+ public function test_get_item() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/mock-col-slug' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+
+ $response_data = $response->get_data();
+ $this->assertArrayHasKey( 'name', $response_data, 'Response data does not have the name key.' );
+ $this->assertArrayHasKey( 'slug', $response_data, 'Response data does not have the slug key.' );
+ $this->assertArrayHasKey( 'description', $response_data, 'Response data does not have the description key.' );
+ $this->assertArrayHasKey( 'font_families', $response_data, 'Response data does not have the font_families key.' );
+ $this->assertArrayHasKey( 'categories', $response_data, 'Response data does not have the categories key.' );
+
+ $this->assertIsString( $response_data['name'], 'name is not a string.' );
+ $this->assertIsString( $response_data['slug'], 'slug is not a string.' );
+ $this->assertIsString( $response_data['description'], 'description is not a string.' );
+
+ $this->assertIsArray( $response_data['font_families'], 'font_families is not an array.' );
+ $this->assertIsArray( $response_data['categories'], 'categories is not an array.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::get_item
+ */
+ public function test_get_item_invalid_slug() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/non-existing-collection' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_font_collection_not_found', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::get_item
+ */
+ public function test_get_item_invalid_collection() {
+ $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' );
+
+ wp_set_current_user( self::$admin_id );
+ $slug = 'invalid-collection';
+ wp_register_font_collection( $slug, 'invalid-collection-file' );
+
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/' . $slug );
+ $response = rest_get_server()->dispatch( $request );
+
+ wp_unregister_font_collection( $slug );
+
+ $this->assertErrorResponse( 'font_collection_json_missing', $response, 500, 'When the collection json file is invalid, the response should return an error for "font_collection_json_missing" with 500 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Collections_Controller::get_item
+ */
+ public function test_get_item_invalid_id_permission() {
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/mock-col-slug' );
+
+ wp_set_current_user( 0 );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response status should be 401 for non-authenticated users.' );
+
+ wp_set_current_user( self::$editor_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response status should be 403 for users without the right permissions.' );
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_context_param() {
+ // Controller does not use get_context_param().
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_create_item() {
+ // Controller does not use test_create_item().
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_update_item() {
+ // Controller does not use test_update_item().
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_delete_item() {
+ // Controller does not use test_delete_item().
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_prepare_item() {
+ // Controller does not use test_prepare_item().
+ }
+
+ public function test_get_item_schema() {
+ $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-collections' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $properties = $data['schema']['properties'];
+ $this->assertCount( 5, $properties, 'There should be 5 properties in the response data schema.' );
+ $this->assertArrayHasKey( 'slug', $properties, 'The slug property should exist in the response data schema.' );
+ $this->assertArrayHasKey( 'name', $properties, 'The name property should exist in the response data schema.' );
+ $this->assertArrayHasKey( 'description', $properties, 'The description property should exist in the response data schema.' );
+ $this->assertArrayHasKey( 'font_families', $properties, 'The slug font_families should exist in the response data schema.' );
+ $this->assertArrayHasKey( 'categories', $properties, 'The categories property should exist in the response data schema.' );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpRestFontFacesControllerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1076 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Font_Faces_Controller_Test functionality.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.5.0
+ *
+ * @group restapi
+ * @group fonts
+ * @group font-library
+ *
+ * @coversDefaultClass WP_REST_Font_Faces_Controller
+ */
+class Tests_REST_WpRestFontFacesController extends WP_Test_REST_Controller_Testcase {
+ protected static $admin_id;
+ protected static $editor_id;
+
+ protected static $font_family_id;
+ protected static $other_font_family_id;
+
+ protected static $font_face_id1;
+ protected static $font_face_id2;
+
+ private static $post_ids_for_cleanup = array();
+
+ protected static $default_settings = array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '400',
+ 'fontStyle' => 'normal',
+ 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf',
+ );
+
+ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
+ self::$font_family_id = Tests_REST_WpRestFontFamiliesController::create_font_family_post();
+ self::$other_font_family_id = Tests_REST_WpRestFontFamiliesController::create_font_family_post();
+
+ self::$font_face_id1 = self::create_font_face_post(
+ self::$font_family_id,
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '400',
+ 'fontStyle' => 'normal',
+ 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ),
+ )
+ );
+ self::$font_face_id2 = self::create_font_face_post(
+ self::$font_family_id,
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '900',
+ 'fontStyle' => 'normal',
+ 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ),
+ )
+ );
+
+ self::$admin_id = $factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ self::$editor_id = $factory->user->create(
+ array(
+ 'role' => 'editor',
+ )
+ );
+
+ self::$post_ids_for_cleanup = array();
+ }
+
+ public static function wpTearDownAfterClass() {
+ self::delete_user( self::$admin_id );
+ self::delete_user( self::$editor_id );
+
+ wp_delete_post( self::$font_family_id, true );
+ wp_delete_post( self::$other_font_family_id, true );
+ wp_delete_post( self::$font_face_id1, true );
+ wp_delete_post( self::$font_face_id2, true );
+ }
+
+ public function tear_down() {
+ foreach ( self::$post_ids_for_cleanup as $post_id ) {
+ wp_delete_post( $post_id, true );
+ }
+ self::$post_ids_for_cleanup = array();
+ parent::tear_down();
+ }
+
+ public static function create_font_face_post( $parent_id, $settings = array() ) {
+ $settings = array_merge( self::$default_settings, $settings );
+ $title = WP_Font_Utils::get_font_face_slug( $settings );
+ $post_id = self::factory()->post->create(
+ wp_slash(
+ array(
+ 'post_type' => 'wp_font_face',
+ 'post_status' => 'publish',
+ 'post_title' => $title,
+ 'post_name' => sanitize_title( $title ),
+ 'post_content' => wp_json_encode( $settings ),
+ 'post_parent' => $parent_id,
+ )
+ )
+ );
+
+ self::$post_ids_for_cleanup[] = $post_id;
+
+ return $post_id;
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::register_routes
+ */
+ public function test_register_routes() {
+ $routes = rest_get_server()->get_routes();
+ $this->assertArrayHasKey(
+ '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces',
+ $routes,
+ 'Font faces collection for the given font family does not exist'
+ );
+ $this->assertCount(
+ 2,
+ $routes['/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces'],
+ 'Font faces collection for the given font family does not have exactly two elements'
+ );
+ $this->assertArrayHasKey(
+ '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)',
+ $routes,
+ 'Single font face route for the given font family does not exist'
+ );
+ $this->assertCount(
+ 2,
+ $routes['/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)'],
+ 'Font faces collection for the given font family does not have exactly two elements'
+ );
+ }
+
+ public function test_font_faces_no_autosave_routes() {
+ $routes = rest_get_server()->get_routes();
+ $this->assertArrayNotHasKey(
+ '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)/autosaves',
+ $routes,
+ 'Font faces autosaves route exists.'
+ );
+ $this->assertArrayNotHasKey(
+ '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<parent>[\d]+)/autosaves/(?P<id>[\d]+)',
+ $routes,
+ 'Font faces autosaves by id route exists.'
+ );
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_context_param() {
+ // See test_get_context_param().
+ }
+
+ /**
+ * @dataProvider data_get_context_param
+ *
+ * @covers WP_REST_Font_Faces_Controller::get_context_param
+ *
+ * @param bool $single_route Whether to test a single route.
+ */
+ public function test_get_context_param( $single_route ) {
+ $route = '/wp/v2/font-families/' . self::$font_family_id . '/font-faces';
+ if ( $single_route ) {
+ $route .= '/' . self::$font_face_id1;
+ }
+
+ $request = new WP_REST_Request( 'OPTIONS', $route );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $endpoint_data = $data['endpoints'][0];
+ $this->assertArrayNotHasKey( 'allow_batch', $endpoint_data, 'The allow_batch property should not exist in the endpoint data.' );
+ $this->assertSame( 'view', $endpoint_data['args']['context']['default'], 'The endpoint\'s args::context::default should be set to view.' );
+ $this->assertSame( array( 'view', 'embed', 'edit' ), $endpoint_data['args']['context']['enum'], 'The endpoint\'s args::context::enum should be set to [ view, embed, edit ].' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_get_context_param() {
+ return array(
+ 'Collection' => array( false ),
+ 'Single' => array( true ),
+ );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_items
+ */
+ public function test_get_items() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200' );
+ $this->assertCount( 2, $data, 'There should be 2 properties in the response data.' );
+ $this->assertArrayHasKey( '_links', $data[0], 'The _links property should exist in the response data 0.' );
+ $this->check_font_face_data( $data[0], self::$font_face_id2, $data[0]['_links'] );
+ $this->assertArrayHasKey( '_links', $data[1], 'The _links property should exist in the response data 1.' );
+ $this->check_font_face_data( $data[1], self::$font_face_id1, $data[1]['_links'] );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_items
+ */
+ public function test_get_items_no_permission() {
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' );
+
+ wp_set_current_user( self::$editor_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_items
+ */
+ public function test_get_items_missing_parent() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_get_item() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->check_font_face_data( $data, self::$font_face_id1, $response->get_links() );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response
+ */
+ public function test_get_item_removes_extra_settings() {
+ $font_face_id = self::create_font_face_post( self::$font_family_id, array( 'extra' => array() ) );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' );
+ $this->assertArrayNotHasKey( 'extra', $data['font_face_settings'], 'The extra property should exist in the font_face_settings data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response
+ */
+ public function test_get_item_malformed_post_content_returns_empty_settings() {
+ $font_face_id = wp_insert_post(
+ array(
+ 'post_type' => 'wp_font_face',
+ 'post_parent' => self::$font_family_id,
+ 'post_status' => 'publish',
+ 'post_content' => 'invalid',
+ )
+ );
+
+ self::$post_ids_for_cleanup[] = $font_face_id;
+
+ $empty_settings = array(
+ 'fontFamily' => '',
+ 'src' => array(),
+ );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' );
+ $this->assertSame( $empty_settings, $data['font_face_settings'], 'The empty settings should exist in the font_face_settings data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_get_item_invalid_font_face_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_get_item_no_permission() {
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' );
+
+ wp_set_current_user( self::$editor_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_get_item_missing_parent() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces/' . self::$font_face_id1 );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_get_item_valid_parent_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_get_item_invalid_parent_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$other_font_family_id . '/font-faces/' . self::$font_face_id1 );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_font_face_parent_id_mismatch', $response, 404 );
+
+ $expected_message = 'The font face does not belong to the specified font family with id of "' . self::$other_font_family_id . '".';
+ $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item() {
+ wp_set_current_user( self::$admin_id );
+ $files = $this->setup_font_file_upload( array( 'woff2' ) );
+
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '200',
+ 'fontStyle' => 'normal',
+ 'src' => array_keys( $files )[0],
+ )
+ )
+ );
+ $request->set_file_params( $files );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->check_font_face_data( $data, $data['id'], $response->get_links() );
+ $this->check_file_meta( $data['id'], array( $data['font_face_settings']['src'] ) );
+
+ $settings = $data['font_face_settings'];
+ unset( $settings['src'] );
+ $this->assertSame(
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '200',
+ 'fontStyle' => 'normal',
+ ),
+ $settings,
+ 'The font_face_settings data should match the expected data.'
+ );
+
+ $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item_with_multiple_font_files() {
+ wp_set_current_user( self::$admin_id );
+ $files = $this->setup_font_file_upload( array( 'ttf', 'otf', 'woff', 'woff2' ) );
+
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '200',
+ 'fontStyle' => 'normal',
+ 'src' => array_keys( $files ),
+ )
+ )
+ );
+ $request->set_file_params( $files );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->check_font_face_data( $data, $data['id'], $response->get_links() );
+ $this->check_file_meta( $data['id'], $data['font_face_settings']['src'] );
+
+ $settings = $data['font_face_settings'];
+ $this->assertCount( 4, $settings['src'], 'There should be 4 items in the font_face_settings::src data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item_invalid_file_type() {
+ $image_file = DIR_TESTDATA . '/images/canola.jpg';
+ $image_path = wp_tempnam( 'canola.jpg' );
+ copy( $image_file, $image_path );
+
+ $files = array(
+ 'file-0' => array(
+ 'name' => 'canola.jpg',
+ 'full_path' => 'canola.jpg',
+ 'type' => 'font/woff2',
+ 'tmp_name' => $image_path,
+ 'error' => 0,
+ 'size' => filesize( $image_path ),
+ ),
+ );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array_merge(
+ self::$default_settings,
+ array(
+ 'fontWeight' => '200',
+ 'src' => array_keys( $files )[0],
+ )
+ )
+ )
+ );
+ $request->set_file_params( $files );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_font_upload_invalid_file_type', $response, 400 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item_with_url_src() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '200',
+ 'fontStyle' => 'normal',
+ 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf',
+ )
+ )
+ );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->check_font_face_data( $data, $data['id'], $response->get_links() );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item_with_all_properties() {
+ wp_set_current_user( self::$admin_id );
+
+ $properties = array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '300 500',
+ 'fontStyle' => 'oblique 30deg 50deg',
+ 'fontDisplay' => 'swap',
+ 'fontStretch' => 'expanded',
+ 'ascentOverride' => '70%',
+ 'descentOverride' => '30%',
+ 'fontVariant' => 'normal',
+ 'fontFeatureSettings' => '"swsh" 2',
+ 'fontVariationSettings' => '"xhgt" 0.7',
+ 'lineGapOverride' => '10%',
+ 'sizeAdjust' => '90%',
+ 'unicodeRange' => 'U+0025-00FF, U+4??',
+ 'preview' => 'https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg',
+ 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf',
+ );
+
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_face_settings', wp_json_encode( $properties ) );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ wp_delete_post( $data['id'], true );
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' );
+ $this->assertSame( $properties, $data['font_face_settings'], 'The font_face_settings should match the expected properties.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item_missing_parent() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces' );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode( array_merge( self::$default_settings, array( 'fontWeight' => '100' ) ) )
+ );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ public function test_create_item_with_duplicate_properties() {
+ $settings = array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '200',
+ 'fontStyle' => 'italic',
+ 'src' => home_url( '/wp-content/fonts/open-sans-italic-light.ttf' ),
+ );
+ self::create_font_face_post( self::$font_family_id, $settings );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'font_face_settings', wp_json_encode( $settings ) );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_duplicate_font_face', $response, 400, 'The response should return an error for "rest_duplicate_font_face" with 400 status.' );
+ $expected_message = 'A font face matching those settings already exists.';
+ $message = $response->as_error()->get_error_messages()[0];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_request
+ */
+ public function test_create_item_default_theme_json_version() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '200',
+ 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf',
+ )
+ )
+ );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ wp_delete_post( $data['id'], true );
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in the response data.' );
+ $this->assertSame( WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The default theme.json version should match the latest version supported by the controller.' );
+ }
+
+ /**
+ * @dataProvider data_create_item_invalid_theme_json_version
+ *
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ *
+ * @param int $theme_json_version Version input to test.
+ */
+ public function test_create_item_invalid_theme_json_version( $theme_json_version ) {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', $theme_json_version );
+ $request->set_param( 'font_face_settings', '' );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_create_item_invalid_theme_json_version() {
+ return array(
+ array( 1 ),
+ array( 3 ),
+ );
+ }
+
+ /**
+ * @dataProvider data_create_item_invalid_settings
+ *
+ * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings
+ *
+ * @param mixed $settings Settings to test.
+ */
+ public function test_create_item_invalid_settings( $settings ) {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_face_settings', wp_json_encode( $settings ) );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_create_item_invalid_settings() {
+ return array(
+ 'Missing fontFamily' => array(
+ 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ),
+ ),
+ 'Empty fontFamily' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ),
+ ),
+ 'Wrong fontFamily type' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => 1234 ) ),
+ ),
+ 'Invalid fontDisplay' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'fontDisplay' => 'invalid' ) ),
+ ),
+ 'Missing src' => array(
+ 'settings' => array_diff_key( self::$default_settings, array( 'src' => '' ) ),
+ ),
+ 'Empty src string' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'src' => '' ) ),
+ ),
+ 'Empty src array' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'src' => array() ) ),
+ ),
+ 'Empty src array values' => array(
+ 'settings' => array_merge( self::$default_settings, array( '', '' ) ),
+ ),
+ 'Wrong src type' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'src' => 1234 ) ),
+ ),
+ 'Wrong src array types' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'src' => array( 1234, 5678 ) ) ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings
+ */
+ public function test_create_item_invalid_settings_json() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_face_settings', 'invalid' );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' );
+ $expected_message = 'font_face_settings parameter must be a valid JSON string.';
+ $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings'];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings
+ */
+ public function test_create_item_invalid_file_src() {
+ $files = $this->setup_font_file_upload( array( 'woff2' ) );
+
+ wp_set_current_user( self::$admin_id );
+ $src = 'invalid';
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array_merge( self::$default_settings, array( 'src' => $src ) )
+ )
+ );
+ $request->set_file_params( $files );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' );
+ $expected_message = 'font_face_settings[src] value "' . $src . '" must be a valid URL or file reference.';
+ $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings'];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings
+ */
+ public function test_create_item_missing_file_src() {
+ $files = $this->setup_font_file_upload( array( 'woff2', 'woff' ) );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param(
+ 'font_face_settings',
+ wp_json_encode(
+ array_merge( self::$default_settings, array( 'src' => array( array_keys( $files )[0] ) ) )
+ )
+ );
+ $request->set_file_params( $files );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' );
+ $expected_message = 'File ' . array_keys( $files )[1] . ' must be used in font_face_settings[src].';
+ $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings'];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @dataProvider data_sanitize_font_face_settings
+ *
+ * @covers WP_REST_Font_Face_Controller::sanitize_font_face_settings
+ *
+ * @param string $settings Settings to test.
+ * @param string $expected Expected settings result.
+ */
+ public function test_create_item_sanitize_font_face_settings( $settings, $expected ) {
+ $settings = array_merge( self::$default_settings, $settings );
+ $expected = array_merge( self::$default_settings, $expected );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $request->set_param( 'font_face_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ wp_delete_post( $data['id'], true );
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->assertSame( $expected, $data['font_face_settings'], 'The response font_face_settings should match.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_sanitize_font_face_settings() {
+ return array(
+ 'settings with tags, extra whitespace, new lines' => array(
+ 'settings' => array(
+ 'fontFamily' => " Open Sans</style><script>alert('XSS');</script>\n ",
+ 'fontStyle' => " oblique 20deg 50deg</style><script>alert('XSS');</script>\n ",
+ 'fontWeight' => " 200</style><script>alert('XSS');</script>\n ",
+ 'src' => " https://example.com/</style><script>alert('XSS');</script> ",
+ 'fontStretch' => " expanded</style><script>alert('XSS');</script>\n ",
+ 'ascentOverride' => " 70%</style><script>alert('XSS');</script>\n ",
+ 'descentOverride' => " 30%</style><script>alert('XSS');</script>\n ",
+ 'fontVariant' => " normal</style><script>alert('XSS');</script>\n ",
+ 'fontFeatureSettings' => " \"swsh\" 2</style><script>alert('XSS');</script>\n ",
+ 'fontVariationSettings' => " \"xhgt\" 0.7</style><script>alert('XSS');</script>\n ",
+ 'lineGapOverride' => " 10%</style><script>alert('XSS');</script>\n ",
+ 'sizeAdjust' => " 90%</style><script>alert('XSS');</script>\n ",
+ 'unicodeRange' => " U+0025-00FF, U+4??</style><script>alert('XSS');</script>\n ",
+ 'preview' => " https://example.com/</style><script>alert('XSS');</script> ",
+ ),
+ 'expected' => array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontStyle' => 'oblique 20deg 50deg',
+ 'fontWeight' => '200',
+ 'src' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20',
+ 'fontStretch' => 'expanded',
+ 'ascentOverride' => '70%',
+ 'descentOverride' => '30%',
+ 'fontVariant' => 'normal',
+ 'fontFeatureSettings' => '"swsh" 2',
+ 'fontVariationSettings' => '"xhgt" 0.7',
+ 'lineGapOverride' => '10%',
+ 'sizeAdjust' => '90%',
+ 'unicodeRange' => 'U+0025-00FF, U+4??',
+ 'preview' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20',
+ ),
+ ),
+ 'multiword font family name with integer' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Libre Barcode 128 Text',
+ ),
+ 'expected' => array(
+ 'fontFamily' => '"Libre Barcode 128 Text"',
+ ),
+ ),
+ 'multiword font family name' => array(
+ 'settings' => array(
+ 'fontFamily' => 'B612 Mono',
+ ),
+ 'expected' => array(
+ 'fontFamily' => '"B612 Mono"',
+ ),
+ ),
+ 'comma-separated font family names' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Open Sans, Noto Sans, sans-serif',
+ ),
+ 'expected' => array(
+ 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif',
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::create_item
+ */
+ // public function test_create_item_no_permission() {}
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::update_item
+ */
+ public function test_update_item() {
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::delete_item
+ */
+ public function test_delete_item() {
+ wp_set_current_user( self::$admin_id );
+ $font_face_id = self::create_font_face_post( self::$font_family_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id );
+ $request->set_param( 'force', true );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 201.' );
+ $this->assertNull( get_post( $font_face_id ), 'The deleted post should not exist.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::delete_item
+ */
+ public function test_delete_item_no_trash() {
+ wp_set_current_user( self::$admin_id );
+ $font_face_id = self::create_font_face_post( self::$font_family_id );
+
+ // Attempt trashing.
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'The response should return an error for "rest_trash_not_supported" with 501 status.' );
+
+ $request->set_param( 'force', 'false' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'When "force" is false, the response should return an error for "rest_trash_not_supported" with 501 status.' );
+
+ // Ensure the post still exists.
+ $post = get_post( $font_face_id );
+ $this->assertNotEmpty( $post, 'The post should still exists.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::delete_item
+ */
+ public function test_delete_item_invalid_font_face_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+ $request->set_param( 'force', true );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::delete
+ */
+ public function test_delete_item_missing_parent() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces/' . self::$font_face_id1 );
+ $request->set_param( 'force', true );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item
+ */
+ public function test_delete_item_invalid_parent_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$other_font_family_id . '/font-faces/' . self::$font_face_id1 );
+ $request->set_param( 'force', true );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_font_face_parent_id_mismatch', $response, 404, 'The response should return an error for "rest_font_face_parent_id_mismatch" with 404 status.' );
+
+ $expected_message = 'The font face does not belong to the specified font family with id of "' . self::$other_font_family_id . '".';
+ $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::delete_item
+ */
+ public function test_delete_item_no_permissions() {
+ $font_face_id = $this->create_font_face_post( self::$font_family_id );
+
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_delete', $response, 401, 'The response should return an error for "rest_cannot_delete" with 401 status for an invalid user.' );
+
+ wp_set_current_user( self::$editor_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_delete', $response, 403, 'The response should return an error for "rest_cannot_delete" with 403 status for a user without permission.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response
+ */
+ public function test_prepare_item() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id2 );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->check_font_face_data( $data, self::$font_face_id2, $response->get_links() );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item_schema
+ */
+ public function test_get_item_schema() {
+ $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $properties = $data['schema']['properties'];
+ $this->assertCount( 4, $properties, 'There should be 4 properties in the schema::properties data.' );
+ $this->assertArrayHasKey( 'id', $properties, 'The id property should exist in the schema::properties data.' );
+ $this->assertArrayHasKey( 'theme_json_version', $properties, 'The theme_json_version property should exist in the schema::properties data.' );
+ $this->assertArrayHasKey( 'parent', $properties, 'The parent property should exist in the schema::properties data.' );
+ $this->assertArrayHasKey( 'font_face_settings', $properties, 'The font_face_settings property should exist in the schema::properties data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_item_schema
+ */
+ public function test_get_item_schema_font_face_settings_should_all_have_sanitize_callbacks() {
+ $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_item_schema();
+ $font_face_settings_schema = $schema['properties']['font_face_settings'];
+
+ $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' );
+ $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' );
+
+ // arg_options should be removed for each setting property.
+ foreach ( $font_face_settings_schema['properties'] as $property ) {
+ $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' );
+ $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' );
+ $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'The sanitize_callback value should be callable.' );
+ }
+ }
+
+ /**
+ * @covers WP_REST_Font_Faces_Controller::get_public_item_schema
+ */
+ public function test_get_public_item_schema_should_not_have_arg_options() {
+ $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_public_item_schema();
+ $font_face_settings_schema = $schema['properties']['font_face_settings'];
+
+ $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' );
+ $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' );
+
+ // arg_options should be removed for each setting property.
+ foreach ( $font_face_settings_schema['properties'] as $property ) {
+ $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' );
+ }
+ }
+
+ /**
+ * If WP_Theme_JSON::LATEST_SCHEMA is changed, the controller should be updated to handle any differences
+ * in `fontFace` structure to ensure support for the latest theme.json schema, and backwards compatibility
+ * for existing wp_font_face posts.
+ */
+ public function test_controller_supports_latest_theme_json_version() {
+ $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ }
+
+ protected function check_font_face_data( $data, $post_id, $links ) {
+ self::$post_ids_for_cleanup[] = $post_id;
+ $post = get_post( $post_id );
+
+ $this->assertArrayHasKey( 'id', $data, 'The id property should exist in response data.' );
+ $this->assertSame( $post->ID, $data['id'], 'The "id" from the response data should match the post ID.' );
+
+ $this->assertArrayHasKey( 'parent', $data, 'The parent property should exist in response data.' );
+ $this->assertSame( $post->post_parent, $data['parent'], 'The "parent" from the response data should match the post parent.' );
+
+ $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in response data.' );
+ $this->assertSame( WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The "theme_json_version" from the response data should match the latest version supported by the controller.' );
+
+ $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in response data.' );
+ $this->assertSame( $post->post_content, wp_json_encode( $data['font_face_settings'] ), 'The encoded "font_face_settings" from the response data should match the post content.' );
+
+ $this->assertNotEmpty( $links, 'The links should not be empty in the response data.' );
+ $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces/' . $post->ID );
+ $this->assertSame( $expected, $links['self'][0]['href'], 'The links URL from the response data should match the post\'s REST endpoint.' );
+ $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces' );
+ $this->assertSame( $expected, $links['collection'][0]['href'], 'The links collection URL from the response data should match the REST endpoint.' );
+ $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent );
+ $this->assertSame( $expected, $links['parent'][0]['href'], 'The links for a parent URL from the response data should match the parent\'s REST endpoint.' );
+ }
+
+ protected function check_file_meta( $font_face_id, $src_attributes ) {
+ $file_meta = get_post_meta( $font_face_id, '_wp_font_face_file' );
+
+ foreach ( $src_attributes as $src_attribute ) {
+ $file_name = basename( $src_attribute );
+ $this->assertContains( $file_name, $file_meta, 'The uploaded font file path should be saved in the post meta.' );
+ }
+ }
+
+ protected function setup_font_file_upload( $formats ) {
+ $files = array();
+ foreach ( $formats as $format ) {
+ $font_file = DIR_TESTDATA . '/fonts/OpenSans-Regular.' . $format;
+ $font_path = wp_tempnam( 'OpenSans-Regular.' . $format );
+ copy( $font_file, $font_path );
+
+ $files[ 'file-' . count( $files ) ] = array(
+ 'name' => 'OpenSans-Regular.' . $format,
+ 'full_path' => 'OpenSans-Regular.' . $format,
+ 'type' => 'font/' . $format,
+ 'tmp_name' => $font_path,
+ 'error' => 0,
+ 'size' => filesize( $font_path ),
+ );
+ }
+
+ return $files;
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpRestFontFamiliesControllerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1057 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Font_Families_Controller_Test functionality.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.5.0
+ *
+ * @group restapi
+ * @group fonts
+ * @group font-library
+ *
+ * @coversDefaultClass WP_REST_Font_Families_Controller
+ */
+class Tests_REST_WpRestFontFamiliesController extends WP_Test_REST_Controller_Testcase {
+ protected static $admin_id;
+ protected static $editor_id;
+
+ protected static $font_family_id1;
+ protected static $font_family_id2;
+
+ protected static $font_face_id1;
+ protected static $font_face_id2;
+
+ private static $post_ids_to_cleanup = array();
+
+ protected static $default_settings = array(
+ 'name' => 'Open Sans',
+ 'slug' => 'open-sans',
+ 'fontFamily' => '"Open Sans", sans-serif',
+ 'preview' => 'https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg',
+ );
+
+ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
+ self::$admin_id = $factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ self::$editor_id = $factory->user->create(
+ array(
+ 'role' => 'editor',
+ )
+ );
+
+ self::$font_family_id1 = self::create_font_family_post(
+ array(
+ 'name' => 'Open Sans',
+ 'slug' => 'open-sans',
+ 'fontFamily' => '"Open Sans", sans-serif',
+ 'preview' => 'https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg',
+ )
+ );
+ self::$font_family_id2 = self::create_font_family_post(
+ array(
+ 'name' => 'Helvetica',
+ 'slug' => 'helvetica',
+ 'fontFamily' => 'Helvetica, Arial, sans-serif',
+ )
+ );
+ self::$font_face_id1 = Tests_REST_WpRestFontFacesController::create_font_face_post(
+ self::$font_family_id1,
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '400',
+ 'fontStyle' => 'normal',
+ 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ),
+ )
+ );
+ self::$font_face_id2 = Tests_REST_WpRestFontFacesController::create_font_face_post(
+ self::$font_family_id1,
+ array(
+ 'fontFamily' => '"Open Sans"',
+ 'fontWeight' => '900',
+ 'fontStyle' => 'normal',
+ 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ),
+ )
+ );
+
+ static::$post_ids_to_cleanup = array();
+ }
+
+ public static function wpTearDownAfterClass() {
+ self::delete_user( self::$admin_id );
+ self::delete_user( self::$editor_id );
+
+ wp_delete_post( self::$font_family_id1 );
+ wp_delete_post( self::$font_family_id2 );
+ wp_delete_post( self::$font_face_id1 );
+ wp_delete_post( self::$font_face_id2 );
+ }
+
+ public function tear_down() {
+ foreach ( static::$post_ids_to_cleanup as $post_id ) {
+ wp_delete_post( $post_id, true );
+ }
+ static::$post_ids_to_cleanup = array();
+
+ parent::tear_down();
+ }
+
+ public static function create_font_family_post( $settings = array() ) {
+ $settings = array_merge( self::$default_settings, $settings );
+ $post_id = self::factory()->post->create(
+ wp_slash(
+ array(
+ 'post_type' => 'wp_font_family',
+ 'post_status' => 'publish',
+ 'post_title' => $settings['name'],
+ 'post_name' => $settings['slug'],
+ 'post_content' => wp_json_encode(
+ array(
+ 'fontFamily' => $settings['fontFamily'],
+ 'preview' => $settings['preview'],
+ )
+ ),
+ )
+ )
+ );
+
+ static::$post_ids_to_cleanup[] = $post_id;
+
+ return $post_id;
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::register_routes
+ */
+ public function test_register_routes() {
+ $routes = rest_get_server()->get_routes();
+ $this->assertArrayHasKey(
+ '/wp/v2/font-families',
+ $routes,
+ 'Font faces collection for the given font family does not exist'
+ );
+ $this->assertCount(
+ 2,
+ $routes['/wp/v2/font-families'],
+ 'Font faces collection for the given font family does not have exactly two elements'
+ );
+ $this->assertArrayHasKey(
+ '/wp/v2/font-families/(?P<id>[\d]+)',
+ $routes,
+ 'Single font face route for the given font family does not exist'
+ );
+ $this->assertCount(
+ 3,
+ $routes['/wp/v2/font-families/(?P<id>[\d]+)'],
+ 'Font faces collection for the given font family does not have exactly two elements'
+ );
+ }
+
+ public function test_font_families_no_autosave_routes() {
+ $routes = rest_get_server()->get_routes();
+ $this->assertArrayNotHasKey(
+ '/wp/v2/font-families/(?P<id>[\d]+)/autosaves',
+ $routes,
+ 'Font families autosaves route exists.'
+ );
+ $this->assertArrayNotHasKey(
+ '/wp/v2/font-families/(?P<parent>[\d]+)/autosaves/(?P<id>[\d]+)',
+ $routes,
+ 'Font families autosaves by id route exists.'
+ );
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function test_context_param() {
+ // See test_get_context_param().
+ }
+
+ /**
+ * @dataProvider data_get_context_param
+ *
+ * @covers WP_REST_Font_Families_Controller::get_context_param
+ *
+ * @param bool $single_route Whether to test a single route.
+ */
+ public function test_get_context_param( $single_route ) {
+ $route = '/wp/v2/font-families';
+ if ( $single_route ) {
+ $route .= '/' . self::$font_family_id1;
+ }
+
+ $request = new WP_REST_Request( 'OPTIONS', $route );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $endpoint_data = $data['endpoints'][0];
+ $this->assertArrayNotHasKey( 'allow_batch', $endpoint_data, 'The allow_batch property should not exist in the endpoint data.' );
+ $this->assertSame( 'view', $endpoint_data['args']['context']['default'], 'The endpoint\'s args::context::default should be set to view.' );
+ $this->assertSame( array( 'view', 'embed', 'edit' ), $endpoint_data['args']['context']['enum'], 'The endpoint\'s args::context::enum should be set to [ view, embed, edit ].' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_get_context_param() {
+ return array(
+ 'Collection' => array( false ),
+ 'Single' => array( true ),
+ );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_items
+ */
+ public function test_get_items() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertCount( 2, $data, 'There should be 2 properties in the response data.' );
+ $this->assertArrayHasKey( '_links', $data[0], 'The _links property should exist in the response data 0.' );
+ $this->check_font_family_data( $data[0], self::$font_family_id2, $data[0]['_links'] );
+ $this->assertArrayHasKey( '_links', $data[1], 'The _links property should exist in the response data 1.' );
+ $this->check_font_family_data( $data[1], self::$font_family_id1, $data[1]['_links'] );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_items
+ */
+ public function test_get_items_by_slug() {
+ $font_family = get_post( self::$font_family_id2 );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' );
+ $request->set_param( 'slug', $font_family->post_name );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertCount( 1, $data, 'There should be 2 properties in the response data.' );
+ $this->assertArrayHasKey( 'id', $data[0], 'The id property should exist in the response data.' );
+ $this->assertSame( $font_family->ID, $data[0]['id'], 'The id should match the expected ID in the response data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_items
+ */
+ public function test_get_items_no_permission() {
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' );
+
+ wp_set_current_user( self::$editor_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_item
+ */
+ public function test_get_item() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->check_font_family_data( $data, self::$font_family_id1, $response->get_links() );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::prepare_item_for_response
+ */
+ public function test_get_item_embedded_font_faces() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 );
+ $request->set_param( '_embed', true );
+ $response = rest_get_server()->dispatch( $request );
+ $data = rest_get_server()->response_to_data( $response, true );
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertArrayHasKey( '_embedded', $data, 'The _embedded property should exist in the response data.' );
+ $this->assertArrayHasKey( 'font_faces', $data['_embedded'], 'The font_faces property should exist in _embedded data.' );
+ $this->assertCount( 2, $data['_embedded']['font_faces'], 'There should be 2 font_faces in the _embedded data.' );
+
+ foreach ( $data['_embedded']['font_faces'] as $font_face ) {
+ $this->assertArrayHasKey( 'id', $font_face, 'The id property should exist in the _embedded font_face data.' );
+
+ $font_face_request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 . '/font-faces/' . $font_face['id'] );
+ $font_face_response = rest_get_server()->dispatch( $font_face_request );
+ $font_face_data = rest_get_server()->response_to_data( $font_face_response, true );
+
+ $this->assertSame( $font_face_data, $font_face, 'The embedded font_face data should match when the data from a single request.' );
+ }
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_item
+ */
+ public function test_get_item_removes_extra_settings() {
+ $font_family_id = self::create_font_family_post( array( 'fontFace' => array() ) );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . $font_family_id );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'], 'The fontFace property should not exist in the font_family_settings data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::prepare_item_for_response
+ */
+ public function test_get_item_malformed_post_content_returns_empty_settings() {
+ $font_family_id = wp_insert_post(
+ array(
+ 'post_type' => 'wp_font_family',
+ 'post_status' => 'publish',
+ 'post_content' => 'invalid',
+ )
+ );
+
+ static::$post_ids_to_cleanup[] = $font_family_id;
+
+ $empty_settings = array(
+ 'name' => '',
+ // Slug will default to the post id.
+ 'slug' => (string) $font_family_id,
+ 'fontFamily' => '',
+ 'preview' => '',
+ );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . $font_family_id );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertSame( $empty_settings, $data['font_family_settings'], 'The empty settings should exist in the font_family_settings data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_item
+ */
+ public function test_get_item_invalid_font_family_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_item
+ */
+ public function test_get_item_no_permission() {
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' );
+
+ wp_set_current_user( self::$editor_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::create_item
+ */
+ public function test_create_item() {
+ $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->check_font_family_data( $data, $data['id'], $response->get_links() );
+
+ $reponse_settings = $data['font_family_settings'];
+ $this->assertSame( $settings, $reponse_settings, 'The expected settings should exist in the font_family_settings data.' );
+ $this->assertEmpty( $data['font_faces'], 'The font_faces should be empty or not exist in the response data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::validate_create_font_face_request
+ */
+ public function test_create_item_default_theme_json_version() {
+ $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) );
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ static::$post_ids_to_cleanup[] = $data['id'];
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in the response data.' );
+ $this->assertSame( WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The default theme.json version should match the latest version supported by the controller.' );
+ }
+
+ /**
+ * @dataProvider data_create_item_invalid_theme_json_version
+ *
+ * @covers WP_REST_Font_Families_Controller::create_item
+ *
+ * @param int $theme_json_version Version to test.
+ */
+ public function test_create_item_invalid_theme_json_version( $theme_json_version ) {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'theme_json_version', $theme_json_version );
+ $request->set_param( 'font_family_settings', wp_json_encode( self::$default_settings ) );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_create_item_invalid_theme_json_version() {
+ return array(
+ array( 1 ),
+ array( 3 ),
+ );
+ }
+
+ /**
+ * @dataProvider data_create_item_with_default_preview
+ *
+ * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings
+ *
+ * @param array $settings Settings to test.
+ */
+ public function test_create_item_with_default_preview( $settings ) {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ static::$post_ids_to_cleanup[] = $data['id'];
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $response_settings = $data['font_family_settings'];
+ $this->assertArrayHasKey( 'preview', $response_settings, 'The preview property should exist in the font_family_settings data.' );
+ $this->assertSame( '', $response_settings['preview'], 'The preview data should be an empty string.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_create_item_with_default_preview() {
+ $default_settings = array(
+ 'name' => 'Open Sans',
+ 'slug' => 'open-sans-2',
+ 'fontFamily' => '"Open Sans", sans-serif',
+ );
+ return array(
+ 'No preview param' => array(
+ 'settings' => $default_settings,
+ ),
+ 'Empty preview' => array(
+ 'settings' => array_merge( $default_settings, array( 'preview' => '' ) ),
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider data_sanitize_font_family_settings
+ *
+ * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings
+ *
+ * @param string $settings Font family settings to test.
+ * @param string $expected Expected settings result.
+ */
+ public function test_create_item_santize_font_family_settings( $settings, $expected ) {
+ $settings = array_merge( self::$default_settings, $settings );
+ $expected = array_merge( self::$default_settings, $expected );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ static::$post_ids_to_cleanup[] = $data['id'];
+
+ $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' );
+ $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_sanitize_font_family_settings() {
+ return array(
+ 'settings with tags, extra whitespace, new lines' => array(
+ 'settings' => array(
+ 'name' => " Opening Sans</style><script>alert('XSS');</script>\n ",
+ 'slug' => " OPENing SanS </style><script>alert('XSS');</script>\n ",
+ 'fontFamily' => " Opening Sans</style><script>alert('XSS');</script>\n ",
+ 'preview' => " https://example.com/</style><script>alert('XSS');</script> ",
+ ),
+ 'expected' => array(
+ 'name' => 'Opening Sans',
+ 'slug' => 'opening-sans-alertxss',
+ 'fontFamily' => '"Opening Sans"',
+ 'preview' => "https://example.com//stylescriptalert('XSS');/script%20%20%20%20%20%20",
+ ),
+ ),
+ 'multiword font family name with integer' => array(
+ 'settings' => array(
+ 'slug' => 'libre-barcode-128-text',
+ 'fontFamily' => 'Libre Barcode 128 Text',
+ ),
+ 'expected' => array(
+ 'slug' => 'libre-barcode-128-text',
+ 'fontFamily' => '"Libre Barcode 128 Text"',
+ ),
+ ),
+ 'multiword font family name' => array(
+ 'settings' => array(
+ 'slug' => 'b612-mono',
+ 'fontFamily' => 'B612 Mono',
+ ),
+ 'expected' => array(
+ 'slug' => 'b612-mono',
+ 'fontFamily' => '"B612 Mono"',
+ ),
+ ),
+ 'comma-separated font family names' => array(
+ 'settings' => array(
+ 'slug' => 'open-sans-noto-sans',
+ 'fontFamily' => 'Open Sans, Noto Sans, sans-serif',
+ ),
+ 'expected' => array(
+ 'slug' => 'open-sans-noto-sans',
+ 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif',
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider data_create_item_invalid_settings
+ *
+ * @covers WP_REST_Font_Families_Controller::validate_create_font_face_settings
+ *
+ * @param array $settings Settings to test.
+ */
+ public function test_create_item_invalid_settings( $settings ) {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_create_item_invalid_settings() {
+ return array(
+ 'Missing name' => array(
+ 'settings' => array_diff_key( self::$default_settings, array( 'name' => '' ) ),
+ ),
+ 'Empty name' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'name' => '' ) ),
+ ),
+ 'Wrong name type' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'name' => 1234 ) ),
+ ),
+ 'Missing slug' => array(
+ 'settings' => array_diff_key( self::$default_settings, array( 'slug' => '' ) ),
+ ),
+ 'Empty slug' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'slug' => '' ) ),
+ ),
+ 'Wrong slug type' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'slug' => 1234 ) ),
+ ),
+ 'Missing fontFamily' => array(
+ 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ),
+ ),
+ 'Empty fontFamily' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ),
+ ),
+ 'Wrong fontFamily type' => array(
+ 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => 1234 ) ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_REST_Font_Family_Controller::validate_font_family_settings
+ */
+ public function test_create_item_invalid_settings_json() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_family_settings', 'invalid' );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' );
+ $expected_message = 'font_family_settings parameter must be a valid JSON string.';
+ $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings'];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Family_Controller::create_item
+ */
+ public function test_create_item_with_duplicate_slug() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ $request->set_param( 'font_family_settings', wp_json_encode( array_merge( self::$default_settings, array( 'slug' => 'helvetica' ) ) ) );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_duplicate_font_family', $response, 400, 'The response should return an error for "rest_duplicate_font_family" with 400 status.' );
+ $expected_message = 'A font family with slug "helvetica" already exists.';
+ $message = $response->as_error()->get_error_messages()[0];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::create_item
+ */
+ public function test_create_item_no_permission() {
+ $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) );
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_create', $response, 401, 'The response should return an error for "rest_cannot_create" with 401 status.' );
+
+ wp_set_current_user( self::$editor_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' );
+ $request->set_param(
+ 'font_family_settings',
+ wp_json_encode(
+ array(
+ 'name' => 'Open Sans',
+ 'slug' => 'open-sans',
+ 'fontFamily' => '"Open Sans", sans-serif',
+ 'preview' => 'https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg',
+ )
+ )
+ );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_create', $response, 403, 'The response should return an error for "rest_cannot_create" with 403 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::update_item
+ */
+ public function test_update_item() {
+ wp_set_current_user( self::$admin_id );
+
+ $settings = array(
+ 'name' => 'Open Sans',
+ 'fontFamily' => '"Open Sans, "Noto Sans", sans-serif',
+ 'preview' => 'https://s.w.org/images/fonts/16.9/previews/open-sans/open-sans-400-normal.svg',
+ );
+
+ $font_family_id = self::create_font_family_post( array( 'slug' => 'open-sans-2' ) );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id );
+ $request->set_param(
+ 'font_family_settings',
+ wp_json_encode( $settings )
+ );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->check_font_family_data( $data, $font_family_id, $response->get_links() );
+
+ $expected_settings = array(
+ 'name' => $settings['name'],
+ 'slug' => 'open-sans-2',
+ 'fontFamily' => $settings['fontFamily'],
+ 'preview' => $settings['preview'],
+ );
+ $this->assertSame( $expected_settings, $data['font_family_settings'], 'The response font_family_settings should match expected settings.' );
+ }
+
+ /**
+ * @dataProvider data_update_item_individual_settings
+ *
+ * @covers WP_REST_Font_Families_Controller::update_item
+ *
+ * @param array $settings Settings to test.
+ */
+ public function test_update_item_individual_settings( $settings ) {
+ wp_set_current_user( self::$admin_id );
+
+ $font_family_id = self::create_font_family_post();
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $key = key( $settings );
+ $value = current( $settings );
+ $this->assertArrayHasKey( $key, $data['font_family_settings'], 'The expected key should exist in the font_family_settings data.' );
+ $this->assertSame( $value, $data['font_family_settings'][ $key ], 'The font_family_settings data should match.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_update_item_individual_settings() {
+ return array(
+ array( array( 'name' => 'Opened Sans' ) ),
+ array( array( 'fontFamily' => '"Opened Sans", sans-serif' ) ),
+ array( array( 'preview' => 'https://s.w.org/images/fonts/16.7/previews/opened-sans/opened-sans-400-normal.svg' ) ),
+ // Empty preview is allowed.
+ array( array( 'preview' => '' ) ),
+ );
+ }
+
+ /**
+ * @dataProvider data_sanitize_font_family_settings
+ *
+ * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings
+ *
+ * @param string $settings Font family settings to test.
+ * @param string $expected Expected settings result.
+ */
+ public function test_update_item_santize_font_family_settings( $settings, $expected ) {
+ // Unset/modify slug from the data provider, since we're updating rather than creating.
+ unset( $settings['slug'] );
+ $initial_settings = array( 'slug' => 'open-sans-update' );
+ $expected = array_merge( self::$default_settings, $expected, $initial_settings );
+
+ wp_set_current_user( self::$admin_id );
+ $font_family_id = self::create_font_family_post( $initial_settings );
+ static::$post_ids_to_cleanup[] = $font_family_id;
+
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' );
+ }
+
+ /**
+ * @dataProvider data_update_item_invalid_settings
+ *
+ * @covers WP_REST_Font_Families_Controller::update_item
+ *
+ * @param array $settings Settings to test.
+ */
+ public function test_update_item_empty_settings( $settings ) {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 );
+ $request->set_param(
+ 'font_family_settings',
+ wp_json_encode( $settings )
+ );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_update_item_invalid_settings() {
+ return array(
+ 'Empty name' => array(
+ array( 'name' => '' ),
+ ),
+ 'Wrong name type' => array(
+ array( 'name' => 1234 ),
+ ),
+ 'Empty fontFamily' => array(
+ array( 'fontFamily' => '' ),
+ ),
+ 'Wrong fontFamily type' => array(
+ array( 'fontFamily' => 1234 ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::update_item
+ */
+ public function test_update_item_update_slug_not_allowed() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 );
+ $request->set_param(
+ 'font_family_settings',
+ wp_json_encode( array( 'slug' => 'new-slug' ) )
+ );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' );
+ $expected_message = 'font_family_settings[slug] cannot be updated.';
+ $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings'];
+ $this->assertSame( $expected_message, $message, 'The response error message should match.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::update_item
+ */
+ public function test_update_item_invalid_font_family_id() {
+ $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) );
+
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404, 'The response should return an error for "rest_post_invalid_id" with 404 status.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::update_item
+ */
+ public function test_update_item_no_permission() {
+ $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) );
+
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_edit', $response, 401, 'The response should return an error for "rest_cannot_edit" with 401 status for an invalid user.' );
+
+ wp_set_current_user( self::$editor_id );
+ $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 );
+ $request->set_param( 'font_family_settings', wp_json_encode( $settings ) );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_edit', $response, 403, 'The response should return an error for "rest_cannot_edit" with 403 status for a user without permission.' );
+ }
+
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::delete_item
+ */
+ public function test_delete_item() {
+ wp_set_current_user( self::$admin_id );
+ $font_family_id = self::create_font_family_post();
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id );
+ $request['force'] = true;
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->assertNull( get_post( $font_family_id ), 'The post should not exist after deleting.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::delete_item
+ */
+ public function test_delete_item_no_trash() {
+ wp_set_current_user( self::$admin_id );
+ $font_family_id = self::create_font_family_post();
+
+ // Attempt trashing.
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'The response should return an error for "rest_trash_not_supported" with 501 status.' );
+
+ $request->set_param( 'force', 'false' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'When "force" is false, the response should return an error for "rest_trash_not_supported" with 501 status.' );
+
+ // Ensure the post still exists.
+ $post = get_post( $font_family_id );
+ $this->assertNotEmpty( $post, 'The post should still exist.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::delete_item
+ */
+ public function test_delete_item_invalid_font_family_id() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::delete_item
+ */
+ public function test_delete_item_no_permissions() {
+ $font_family_id = self::create_font_family_post();
+
+ wp_set_current_user( 0 );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_delete', $response, 401, 'The response should return an error for "rest_cannot_delete" with 401 status for an invalid user.' );
+
+ wp_set_current_user( self::$editor_id );
+ $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertErrorResponse( 'rest_cannot_delete', $response, 403, 'The response should return an error for "rest_cannot_delete" with 403 status for a user without permission.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::prepare_item_for_response
+ */
+ public function test_prepare_item() {
+ wp_set_current_user( self::$admin_id );
+ $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id2 );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $this->check_font_family_data( $data, self::$font_family_id2, $response->get_links() );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_item_schema
+ */
+ public function test_get_item_schema() {
+ $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
+ $properties = $data['schema']['properties'];
+ $this->assertCount( 4, $properties, 'There should be 4 properties in the schema::properties data.' );
+ $this->assertArrayHasKey( 'id', $properties, 'The id property should exist in the schema::properties data.' );
+ $this->assertArrayHasKey( 'theme_json_version', $properties, 'The theme_json_version property should exist in the schema::properties data.' );
+ $this->assertArrayHasKey( 'font_faces', $properties, 'The font_faces property should exist in the schema::properties data.' );
+ $this->assertArrayHasKey( 'font_family_settings', $properties, 'The font_family_settings property should exist in the schema::properties data.' );
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_item_schema
+ */
+ public function test_get_item_schema_font_family_settings_should_all_have_sanitize_callbacks() {
+ $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_item_schema();
+ $font_family_settings_schema = $schema['properties']['font_family_settings'];
+
+ $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' );
+ $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' );
+
+ // arg_options should be removed for each setting property.
+ foreach ( $font_family_settings_schema['properties'] as $property ) {
+ $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' );
+ $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' );
+ $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'That sanitize_callback value should be callable.' );
+ }
+ }
+
+ /**
+ * @covers WP_REST_Font_Families_Controller::get_public_item_schema
+ */
+ public function test_get_public_item_schema_should_not_have_arg_options() {
+ $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_public_item_schema();
+ $font_family_settings_schema = $schema['properties']['font_family_settings'];
+
+ $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' );
+ $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' );
+
+ // arg_options should be removed for each setting property.
+ foreach ( $font_family_settings_schema['properties'] as $property ) {
+ $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' );
+ }
+ }
+
+ /**
+ * If WP_Theme_JSON::LATEST_SCHEMA is changed, the controller should be updated to handle any differences
+ * in `fontFamilies` structure to ensure support for the latest theme.json schema, and backwards compatibility
+ * for existing wp_font_family posts.
+ */
+ public function test_controller_supports_latest_theme_json_version() {
+ $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED );
+ }
+
+ protected function check_font_family_data( $data, $post_id, $links ) {
+ static::$post_ids_to_cleanup[] = $post_id;
+ $post = get_post( $post_id );
+
+ $this->assertArrayHasKey( 'id', $data, 'The id property should exist in response data.' );
+ $this->assertSame( $post->ID, $data['id'], 'The "id" from the response data should match the post ID.' );
+
+ $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in response data.' );
+ $this->assertSame( WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The "theme_json_version" from the response data should match the latest version supported by the controller.' );
+
+ $font_face_ids = get_children(
+ array(
+ 'fields' => 'ids',
+ 'post_parent' => $post_id,
+ 'post_type' => 'wp_font_face',
+ 'order' => 'ASC',
+ 'orderby' => 'ID',
+ )
+ );
+ $this->assertArrayHasKey( 'font_faces', $data, 'The font_faces property should exist in the response data.' );
+
+ foreach ( $font_face_ids as $font_face_id ) {
+ $this->assertContains( $font_face_id, $data['font_faces'], 'The ID is in the font_faces data.' );
+ }
+
+ $this->assertArrayHasKey( 'font_family_settings', $data, 'The font_family_settings property should exist in the response data.' );
+ $settings = $data['font_family_settings'];
+ $expected_settings = array(
+ 'name' => $post->post_title,
+ 'slug' => $post->post_name,
+ 'fontFamily' => $settings['fontFamily'],
+ 'preview' => $settings['preview'],
+ );
+ $this->assertSame( $expected_settings, $settings, 'The font_family_settings should match.' );
+
+ $this->assertNotEmpty( $links, 'The links should not be empty in the response data.' );
+ $expected = rest_url( 'wp/v2/font-families/' . $post->ID );
+ $this->assertSame( $expected, $links['self'][0]['href'], 'The links URL from the response data should match the post\'s REST endpoint.' );
+ $expected = rest_url( 'wp/v2/font-families' );
+ $this->assertSame( $expected, $links['collection'][0]['href'], 'The links collection URL from the response data should match the REST endpoint.' );
+
+ if ( ! $font_face_ids ) {
+ return;
+ }
+
+ // Check font_face links, if present.
+ $this->assertArrayHasKey( 'font_faces', $links );
+ foreach ( $links['font_faces'] as $index => $link ) {
+ $expected = rest_url( 'wp/v2/font-families/' . $post->ID . '/font-faces/' . $font_face_ids[ $index ] );
+ $this->assertSame( $expected, $link['href'], 'The links for a font faces URL from the response data should match the REST endpoint.' );
+
+ $embeddable = isset( $link['attributes']['embeddable'] )
+ ? $link['attributes']['embeddable']
+ : $link['embeddable'];
+ $this->assertTrue( $embeddable, 'The embeddable should be true.' );
+ }
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsrestapirestschemasetupphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -189,6 +189,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> '/wp-site-health/v1/directory-sizes',
</span><span class="cx" style="display: block; padding: 0 10px"> '/wp/v2/wp_pattern_category',
</span><span class="cx" style="display: block; padding: 0 10px"> '/wp/v2/wp_pattern_category/(?P<id>[\d]+)',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ '/wp/v2/font-collections',
+ '/wp/v2/font-collections/(?P<slug>[\/\w-]+)',
+ '/wp/v2/font-families',
+ '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces',
+ '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)',
+ '/wp/v2/font-families/(?P<id>[\d]+)',
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertSameSets( $expected_routes, $routes );
</span></span></pre></div>
<a id="trunktestsqunitfixtureswpapigeneratedjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/wp-api-generated.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/wp-api-generated.js 2024-02-07 08:51:39 UTC (rev 57547)
+++ trunk/tests/qunit/fixtures/wp-api-generated.js 2024-02-07 09:18:38 UTC (rev 57548)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6660,6 +6660,381 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> ]
</span><span class="cx" style="display: block; padding: 0 10px"> },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ "/wp/v2/font-families": {
+ "namespace": "wp/v2",
+ "methods": [
+ "GET",
+ "POST"
+ ],
+ "endpoints": [
+ {
+ "methods": [
+ "GET"
+ ],
+ "args": {
+ "context": {
+ "description": "Scope under which the request is made; determines fields present in response.",
+ "type": "string",
+ "enum": [
+ "view",
+ "embed",
+ "edit"
+ ],
+ "default": "view",
+ "required": false
+ },
+ "page": {
+ "description": "Current page of the collection.",
+ "type": "integer",
+ "default": 1,
+ "minimum": 1,
+ "required": false
+ },
+ "per_page": {
+ "description": "Maximum number of items to be returned in result set.",
+ "type": "integer",
+ "default": 10,
+ "minimum": 1,
+ "maximum": 100,
+ "required": false
+ },
+ "exclude": {
+ "description": "Ensure result set excludes specific IDs.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "default": [],
+ "required": false
+ },
+ "include": {
+ "description": "Limit result set to specific IDs.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "default": [],
+ "required": false
+ },
+ "offset": {
+ "description": "Offset the result set by a specific number of items.",
+ "type": "integer",
+ "required": false
+ },
+ "order": {
+ "description": "Order sort attribute ascending or descending.",
+ "type": "string",
+ "default": "desc",
+ "enum": [
+ "asc",
+ "desc"
+ ],
+ "required": false
+ },
+ "orderby": {
+ "description": "Sort collection by post attribute.",
+ "type": "string",
+ "default": "id",
+ "enum": [
+ "id",
+ "include"
+ ],
+ "required": false
+ },
+ "slug": {
+ "description": "Limit result set to posts with one or more specific slugs.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "required": false
+ }
+ }
+ },
+ {
+ "methods": [
+ "POST"
+ ],
+ "args": {
+ "theme_json_version": {
+ "description": "Version of the theme.json schema used for the typography settings.",
+ "type": "integer",
+ "default": 2,
+ "minimum": 2,
+ "maximum": 2,
+ "required": false
+ },
+ "font_family_settings": {
+ "description": "font-family declaration in theme.json format, encoded as a string.",
+ "type": "string",
+ "required": true
+ }
+ }
+ }
+ ],
+ "_links": {
+ "self": [
+ {
+ "href": "http://example.org/index.php?rest_route=/wp/v2/font-families"
+ }
+ ]
+ }
+ },
+ "/wp/v2/font-families/(?P<id>[\\d]+)": {
+ "namespace": "wp/v2",
+ "methods": [
+ "GET",
+ "POST",
+ "PUT",
+ "PATCH",
+ "DELETE"
+ ],
+ "endpoints": [
+ {
+ "methods": [
+ "GET"
+ ],
+ "args": {
+ "id": {
+ "description": "Unique identifier for the post.",
+ "type": "integer",
+ "required": false
+ },
+ "context": {
+ "description": "Scope under which the request is made; determines fields present in response.",
+ "type": "string",
+ "enum": [
+ "view",
+ "embed",
+ "edit"
+ ],
+ "default": "view",
+ "required": false
+ }
+ }
+ },
+ {
+ "methods": [
+ "POST",
+ "PUT",
+ "PATCH"
+ ],
+ "args": {
+ "id": {
+ "description": "Unique identifier for the post.",
+ "type": "integer",
+ "required": false
+ },
+ "theme_json_version": {
+ "description": "Version of the theme.json schema used for the typography settings.",
+ "type": "integer",
+ "default": 2,
+ "minimum": 2,
+ "maximum": 2,
+ "required": false
+ },
+ "font_family_settings": {
+ "description": "font-family declaration in theme.json format, encoded as a string.",
+ "type": "string",
+ "required": true
+ }
+ }
+ },
+ {
+ "methods": [
+ "DELETE"
+ ],
+ "args": {
+ "id": {
+ "description": "Unique identifier for the post.",
+ "type": "integer",
+ "required": false
+ },
+ "force": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether to bypass Trash and force deletion.",
+ "required": false
+ }
+ }
+ }
+ ]
+ },
+ "/wp/v2/font-families/(?P<font_family_id>[\\d]+)/font-faces": {
+ "namespace": "wp/v2",
+ "methods": [
+ "GET",
+ "POST"
+ ],
+ "endpoints": [
+ {
+ "methods": [
+ "GET"
+ ],
+ "args": {
+ "font_family_id": {
+ "description": "The ID for the parent font family of the font face.",
+ "type": "integer",
+ "required": true
+ },
+ "context": {
+ "description": "Scope under which the request is made; determines fields present in response.",
+ "type": "string",
+ "enum": [
+ "view",
+ "embed",
+ "edit"
+ ],
+ "default": "view",
+ "required": false
+ },
+ "page": {
+ "description": "Current page of the collection.",
+ "type": "integer",
+ "default": 1,
+ "minimum": 1,
+ "required": false
+ },
+ "per_page": {
+ "description": "Maximum number of items to be returned in result set.",
+ "type": "integer",
+ "default": 10,
+ "minimum": 1,
+ "maximum": 100,
+ "required": false
+ },
+ "exclude": {
+ "description": "Ensure result set excludes specific IDs.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "default": [],
+ "required": false
+ },
+ "include": {
+ "description": "Limit result set to specific IDs.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "default": [],
+ "required": false
+ },
+ "offset": {
+ "description": "Offset the result set by a specific number of items.",
+ "type": "integer",
+ "required": false
+ },
+ "order": {
+ "description": "Order sort attribute ascending or descending.",
+ "type": "string",
+ "default": "desc",
+ "enum": [
+ "asc",
+ "desc"
+ ],
+ "required": false
+ },
+ "orderby": {
+ "description": "Sort collection by post attribute.",
+ "type": "string",
+ "default": "id",
+ "enum": [
+ "id",
+ "include"
+ ],
+ "required": false
+ }
+ }
+ },
+ {
+ "methods": [
+ "POST"
+ ],
+ "args": {
+ "font_family_id": {
+ "description": "The ID for the parent font family of the font face.",
+ "type": "integer",
+ "required": true
+ },
+ "theme_json_version": {
+ "description": "Version of the theme.json schema used for the typography settings.",
+ "type": "integer",
+ "default": 2,
+ "minimum": 2,
+ "maximum": 2,
+ "required": false
+ },
+ "font_face_settings": {
+ "description": "font-face declaration in theme.json format, encoded as a string.",
+ "type": "string",
+ "required": true
+ }
+ }
+ }
+ ]
+ },
+ "/wp/v2/font-families/(?P<font_family_id>[\\d]+)/font-faces/(?P<id>[\\d]+)": {
+ "namespace": "wp/v2",
+ "methods": [
+ "GET",
+ "DELETE"
+ ],
+ "endpoints": [
+ {
+ "methods": [
+ "GET"
+ ],
+ "args": {
+ "font_family_id": {
+ "description": "The ID for the parent font family of the font face.",
+ "type": "integer",
+ "required": true
+ },
+ "id": {
+ "description": "Unique identifier for the font face.",
+ "type": "integer",
+ "required": true
+ },
+ "context": {
+ "description": "Scope under which the request is made; determines fields present in response.",
+ "type": "string",
+ "enum": [
+ "view",
+ "embed",
+ "edit"
+ ],
+ "default": "view",
+ "required": false
+ }
+ }
+ },
+ {
+ "methods": [
+ "DELETE"
+ ],
+ "args": {
+ "font_family_id": {
+ "description": "The ID for the parent font family of the font face.",
+ "type": "integer",
+ "required": true
+ },
+ "id": {
+ "description": "Unique identifier for the font face.",
+ "type": "integer",
+ "required": true
+ },
+ "force": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether to bypass Trash and force deletion.",
+ "required": false
+ }
+ }
+ }
+ ]
+ },
</ins><span class="cx" style="display: block; padding: 0 10px"> "/wp/v2/media": {
</span><span class="cx" style="display: block; padding: 0 10px"> "namespace": "wp/v2",
</span><span class="cx" style="display: block; padding: 0 10px"> "methods": [
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -8785,6 +9160,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> "wp_template": "wp_template",
</span><span class="cx" style="display: block; padding: 0 10px"> "wp_template_part": "wp_template_part",
</span><span class="cx" style="display: block; padding: 0 10px"> "wp_navigation": "wp_navigation",
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ "wp_font_family": "wp_font_family",
+ "wp_font_face": "wp_font_face",
</ins><span class="cx" style="display: block; padding: 0 10px"> "attachment": "attachment"
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> },
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -11504,6 +11881,80 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> ]
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ },
+ "/wp/v2/font-collections": {
+ "namespace": "wp/v2",
+ "methods": [
+ "GET"
+ ],
+ "endpoints": [
+ {
+ "methods": [
+ "GET"
+ ],
+ "args": {
+ "context": {
+ "description": "Scope under which the request is made; determines fields present in response.",
+ "type": "string",
+ "enum": [
+ "view",
+ "embed",
+ "edit"
+ ],
+ "default": "view",
+ "required": false
+ },
+ "page": {
+ "description": "Current page of the collection.",
+ "type": "integer",
+ "default": 1,
+ "minimum": 1,
+ "required": false
+ },
+ "per_page": {
+ "description": "Maximum number of items to be returned in result set.",
+ "type": "integer",
+ "default": 10,
+ "minimum": 1,
+ "maximum": 100,
+ "required": false
+ }
+ }
+ }
+ ],
+ "_links": {
+ "self": [
+ {
+ "href": "http://example.org/index.php?rest_route=/wp/v2/font-collections"
+ }
+ ]
+ }
+ },
+ "/wp/v2/font-collections/(?P<slug>[\\/\\w-]+)": {
+ "namespace": "wp/v2",
+ "methods": [
+ "GET"
+ ],
+ "endpoints": [
+ {
+ "methods": [
+ "GET"
+ ],
+ "args": {
+ "context": {
+ "description": "Scope under which the request is made; determines fields present in response.",
+ "type": "string",
+ "enum": [
+ "view",
+ "embed",
+ "edit"
+ ],
+ "default": "view",
+ "required": false
+ }
+ }
+ }
+ ]
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> },
</span><span class="cx" style="display: block; padding: 0 10px"> "site_logo": 0,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -12541,6 +12992,66 @@
</span><span class="cx" style="display: block; padding: 0 10px"> ]
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ "wp_font_family": {
+ "description": "",
+ "hierarchical": false,
+ "has_archive": false,
+ "name": "Font Families",
+ "slug": "wp_font_family",
+ "icon": null,
+ "taxonomies": [],
+ "rest_base": "font-families",
+ "rest_namespace": "wp/v2",
+ "_links": {
+ "collection": [
+ {
+ "href": "http://example.org/index.php?rest_route=/wp/v2/types"
+ }
+ ],
+ "wp:items": [
+ {
+ "href": "http://example.org/index.php?rest_route=/wp/v2/font-families"
+ }
+ ],
+ "curies": [
+ {
+ "name": "wp",
+ "href": "https://api.w.org/{rel}",
+ "templated": true
+ }
+ ]
+ }
+ },
+ "wp_font_face": {
+ "description": "",
+ "hierarchical": false,
+ "has_archive": false,
+ "name": "Font Faces",
+ "slug": "wp_font_face",
+ "icon": null,
+ "taxonomies": [],
+ "rest_base": "font-families/(?P<font_family_id>[\\d]+)/font-faces",
+ "rest_namespace": "wp/v2",
+ "_links": {
+ "collection": [
+ {
+ "href": "http://example.org/index.php?rest_route=/wp/v2/types"
+ }
+ ],
+ "wp:items": [
+ {
+ "href": "http://example.org/index.php?rest_route=/wp/v2/font-families/(?P<font_family_id>[\\d]+)/font-faces"
+ }
+ ],
+ "curies": [
+ {
+ "name": "wp",
+ "href": "https://api.w.org/{rel}",
+ "templated": true
+ }
+ ]
+ }
+ },
</ins><span class="cx" style="display: block; padding: 0 10px"> "attachment": {
</span><span class="cx" style="display: block; padding: 0 10px"> "description": "",
</span><span class="cx" style="display: block; padding: 0 10px"> "hierarchical": false,
</span></span></pre>
</div>
</div>
</body>
</html>