From c8072d46847a20172f2c3b83e06569c0e5ce8bcd Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Tue, 9 Jan 2024 10:57:36 -0600 Subject: [PATCH 01/16] Font Library: add wp_font_face post type and scaffold font face REST API controller (#57656) --- .../class-wp-rest-font-faces-controller.php | 461 ++++++++++++++++++ .../fonts/font-library/font-library.php | 36 ++ lib/load.php | 1 + .../wpRestFontFacesController.php | 312 ++++++++++++ 4 files changed, 810 insertions(+) create mode 100644 lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php create mode 100644 phpunit/tests/fonts/font-library/wpRestFontFacesController.php diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php new file mode 100644 index 00000000000000..b81d220a7c7f24 --- /dev/null +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -0,0 +1,461 @@ +post_type = $post_type; + + $post_type_obj = get_post_type_object( $post_type ); + $this->rest_base = $post_type_obj->rest_base; + + $parent_post_type = 'wp_font_family'; + $this->parent_post_type = $parent_post_type; + $parent_post_type_obj = get_post_type_object( $parent_post_type ); + $this->parent_base = $parent_post_type_obj->rest_base; + $this->namespace = $parent_post_type_obj->rest_namespace; + } + + /** + * Registers the routes for posts. + * + * @since 6.5.0 + * + * @see register_rest_route() + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_font_faces_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_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the font face.', 'gutenberg' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_font_faces_permissions_check' ), + 'args' => array(), + ), + 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' ), + ), + ), + ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get the parent font family, if the ID is valid. + * + * @since 6.5.0 + * + * @param int $parent_post_id Supplied ID. + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent( $parent_post_id ) { + $error = new WP_Error( + 'rest_post_invalid_parent', + __( 'Invalid post parent ID.', 'default' ), + array( 'status' => 404 ) + ); + + if ( (int) $parent_post_id <= 0 ) { + return $error; + } + + $parent_post = get_post( (int) $parent_post_id ); + + if ( empty( $parent_post ) || empty( $parent_post->ID ) + || $this->parent_post_type !== $parent_post->post_type + ) { + return $error; + } + + return $parent_post; + } + + /** + * Checks if a given request has access to read posts. + * + * @since 6.5.0 + * + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_font_faces_permissions_check() { + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font faces.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Allow the font face post type to be managed through the REST API. + * + * @since 6.5.0 + * + * @param WP_Post_Type|string $post_type Post type name or object. + * @return bool Whether the post type is allowed in REST. + */ + protected function check_is_post_type_allowed( $post_type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + return true; + } + + /** + * Retrieves a collection of font faces within the parent font family. + * + * @since 4.7.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 ) { + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + return parent::get_items( $request ); + } + + /** + * Retrieves a single font face for within parent font family. + * + * @since 4.7.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; + } + + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + $font_face = parent::get_item( $request ); + + if ( (int) $parent->ID !== (int) $font_face->data['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"', 'gutenberg' ), $parent->ID ), + array( 'status' => 404 ) + ); + } + + return $font_face; + } + + /** + * 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 ) { + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for revisions. + 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.", 'gutenberg' ), 'force=true' ), + array( 'status' => 501 ) + ); + } + + return parent::delete_item( $request ); + } + + /** + * 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. + $links = array( + 'self' => array( + 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base . '/' . $post->ID ), + ), + 'collection' => array( + 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base ), + ), + 'parent' => array( + 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent ), + ), + ); + + return $links; + } + + /** + * 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', + 'readonly' => true, + ), + 'theme_json_version' => array( + 'description' => __( 'Version of the theme.json schema used for the font face typography settings.', 'gutenberg' ), + 'type' => 'integer', + 'default' => 2, + 'minimum' => 2, + 'maximum' => 2, + ), + 'parent' => array( + 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), + 'type' => 'integer', + ), + // Font face settings come directly from theme.json schema + // See https://schemas.wp.org/trunk/theme.json + 'font_face_settings' => array( + 'description' => 'Array of font-face declarations.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'fontFamily' => array( + 'description' => 'CSS font-family value.', + 'type' => 'string', + 'default' => '', + ), + 'fontStyle' => array( + 'description' => 'CSS font-style value.', + 'type' => 'string', + 'default' => 'normal', + ), + 'fontWeight' => array( + 'description' => 'List of available font weights, separated by a space.', + 'default' => '400', + 'oneOf' => array( + array( + 'type' => 'string', + ), + array( + 'type' => 'integer', + ), + ), + ), + 'fontDisplay' => array( + 'description' => 'CSS font-display value.', + 'type' => 'string', + 'default' => 'fallback', + 'enum' => array( + 'auto', + 'block', + 'fallback', + 'swap', + 'optional', + ), + ), + 'src' => array( + 'description' => 'Paths or URLs to the font files.', + 'oneOf' => array( + array( + 'type' => 'string', + ), + array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + 'default' => array(), + ), + 'fontStretch' => array( + 'description' => 'CSS font-stretch value.', + 'type' => 'string', + ), + 'ascentOverride' => array( + 'description' => 'CSS ascent-override value.', + 'type' => 'string', + ), + 'descentOverride' => array( + 'description' => 'CSS descent-override value.', + 'type' => 'string', + ), + 'fontVariant' => array( + 'description' => 'CSS font-variant value.', + 'type' => 'string', + ), + 'fontFeatureSettings' => array( + 'description' => 'CSS font-feature-settings value.', + 'type' => 'string', + ), + 'fontVariationSettings' => array( + 'description' => 'CSS font-variation-settings value.', + 'type' => 'string', + ), + 'lineGapOverride' => array( + 'description' => 'CSS line-gap-override value.', + 'type' => 'string', + ), + 'sizeAdjust' => array( + 'description' => 'CSS size-adjust value.', + 'type' => 'string', + ), + 'unicodeRange' => array( + 'description' => 'CSS unicode-range value.', + 'type' => 'string', + ), + 'preview' => array( + 'description' => 'URL to a preview image of the font face.', + 'type' => 'string', + ), + ), + 'required' => array( 'fontFamily', 'src' ), + 'additionalProperties' => false, + ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the query params for the font face collection. + * + * @since 6.5.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + return array( + 'page' => array( + 'description' => __( 'Current page of the collection.', 'default' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ), + 'per_page' => array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'default' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ), + 'search' => array( + 'description' => __( 'Limit results to those matching a string.', 'default' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } +} diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index d1ad8e1447ad9c..70bb50e5dfd731 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -32,9 +32,45 @@ function gutenberg_init_font_library_routes() { ); register_post_type( 'wp_font_family', $args ); + register_post_type( + 'wp_font_face', + array( + 'labels' => array( + 'name' => __( 'Font Faces', 'gutenberg' ), + 'singular_name' => __( 'Font Face', 'gutenberg' ), + ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'show_in_rest' => false, + 'rest_base' => 'font-faces', + // TODO: Add custom font capability + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'read_post' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'edit_post' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'delete_post' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => false, + 'query_var' => false, + ) + ); + // @core-merge: This code will go into Core's `create_initial_rest_routes()`. $font_collections_controller = new WP_REST_Font_Collections_Controller(); $font_collections_controller->register_routes(); + + $font_faces_controller = new WP_REST_Font_Faces_Controller(); + $font_faces_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); diff --git a/lib/load.php b/lib/load.php index d413334227ee73..6a8df2e38a651c 100644 --- a/lib/load.php +++ b/lib/load.php @@ -151,6 +151,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family-utils.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php'; + require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php'; require __DIR__ . '/experimental/fonts/font-library/font-library.php'; diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php new file mode 100644 index 00000000000000..5e115fab539f66 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -0,0 +1,312 @@ +post->create( array( 'post_type' => 'wp_font_family' ) ); + self::$other_font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); + self::$font_face_id1 = $factory->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + ) + ); + self::$font_face_id2 = $factory->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + ) + ); + + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + } + + /** + * @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[\d]+)/font-faces', + $routes, + 'Font faces collection for the given font family does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/font-families/(?P[\d]+)/font-faces'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + $this->assertArrayHasKey( + '/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)', + $routes, + 'Single font face route for the given font family does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * @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() ); + $this->assertCount( 2, $data ); + $this->assertSame( self::$font_face_id1, $data[0]['id'] ); + $this->assertSame( self::$font_face_id2, $data[1]['id'] ); + } + + /** + * @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 ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + } + + /** + * @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 ); + $this->assertSame( 200, $response->get_status() ); + + $fields = array( + 'id', + 'parent', + ); + $data = $response->get_data(); + $this->assertSameSets( $fields, array_keys( $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 ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + } + + /** + * @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( self::$font_family_id, $data['parent'], "The returned revision's id should match the parent 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 ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $fields = array( + 'id', + 'parent', + ); + $data = $response->get_data(); + $this->assertSameSets( $fields, array_keys( $data ) ); + + $this->assertSame( self::$font_family_id, $data['parent'], "The returned revision's id should match the parent id." ); + } + + 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::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + ) + ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $request['force'] = true; + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertNull( get_post( $font_face_id ) ); + } + + /** + * @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::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => 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 ); + + $request->set_param( 'force', 'false' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); + + // Ensure the post still exists. + $post = get_post( $font_face_id ); + $this->assertNotEmpty( $post ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete_item + */ + public function test_delete_item_invalid_delete_permissions() { + wp_set_current_user( self::$editor_id ); + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_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 ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Not yet using the prepare_item method for font faces. + } + + /** + * @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(); + + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'theme_json_version', $properties ); + $this->assertArrayHasKey( 'parent', $properties ); + $this->assertArrayHasKey( 'font_face_settings', $properties ); + } +} From 61da35c78f8e2d47751c814c45ff9edbeaec4c3d Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Thu, 11 Jan 2024 10:40:33 -0600 Subject: [PATCH 02/16] Font Library: create font faces through the REST API (#57702) --- .../class-wp-rest-font-faces-controller.php | 551 ++++++++++++------ phpunit/data/fonts/OpenSans-Regular.otf | Bin 0 -> 254960 bytes phpunit/data/fonts/OpenSans-Regular.ttf | Bin 0 -> 130976 bytes phpunit/data/fonts/OpenSans-Regular.woff | Bin 0 -> 63712 bytes phpunit/data/fonts/OpenSans-Regular.woff2 | Bin 0 -> 47016 bytes .../wpRestFontFacesController.php | 364 +++++++++++- 6 files changed, 716 insertions(+), 199 deletions(-) create mode 100644 phpunit/data/fonts/OpenSans-Regular.otf create mode 100644 phpunit/data/fonts/OpenSans-Regular.ttf create mode 100644 phpunit/data/fonts/OpenSans-Regular.woff create mode 100644 phpunit/data/fonts/OpenSans-Regular.woff2 diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php index b81d220a7c7f24..c6b51dc17060b9 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -55,12 +55,13 @@ public function __construct() { public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, array( - 'args' => array( - 'parent' => array( + 'args' => array( + 'font_family_id' => array( 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), 'type' => 'integer', + 'required' => true, ), ), array( @@ -73,25 +74,26 @@ public function register_routes() { 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + 'args' => $this->get_create_params(), ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, - '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base . '/(?P[\d]+)', + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base . '/(?P[\d]+)', array( - 'args' => array( - 'parent' => array( + 'args' => array( + 'font_family_id' => array( 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), 'type' => 'integer', + 'required' => true, ), - 'id' => array( + 'id' => array( 'description' => __( 'Unique identifier for the font face.', 'gutenberg' ), 'type' => 'integer', + 'required' => true, ), ), array( @@ -112,42 +114,11 @@ public function register_routes() { ), ), ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } - /** - * Get the parent font family, if the ID is valid. - * - * @since 6.5.0 - * - * @param int $parent_post_id Supplied ID. - * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. - */ - protected function get_parent( $parent_post_id ) { - $error = new WP_Error( - 'rest_post_invalid_parent', - __( 'Invalid post parent ID.', 'default' ), - array( 'status' => 404 ) - ); - - if ( (int) $parent_post_id <= 0 ) { - return $error; - } - - $parent_post = get_post( (int) $parent_post_id ); - - if ( empty( $parent_post ) || empty( $parent_post->ID ) - || $this->parent_post_type !== $parent_post->post_type - ) { - return $error; - } - - return $parent_post; - } - /** * Checks if a given request has access to read posts. * @@ -169,30 +140,61 @@ public function get_font_faces_permissions_check() { return true; } - /** - * Allow the font face post type to be managed through the REST API. - * - * @since 6.5.0 - * - * @param WP_Post_Type|string $post_type Post type name or object. - * @return bool Whether the post type is allowed in REST. - */ - protected function check_is_post_type_allowed( $post_type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + public function validate_create_font_face_settings( $value, $request ) { + $settings = json_decode( $value, true ); + $schema = $this->get_item_schema()['properties']['font_face_settings']; + + // Check that the font face settings match the theme.json schema. + $valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); + + // Some properties trigger a multiple "oneOf" types error that we ignore, because they are still valid. + // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric type checking. + if ( is_wp_error( $valid_settings ) && $valid_settings->get_error_code() !== 'rest_one_of_multiple_matches' ) { + $valid_settings->add_data( array( 'status' => 400 ) ); + return $valid_settings; + } + + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + $files = $request->get_file_params(); + + // 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: %s: A URL. */ + __( 'Every file uploaded must be used as a font face src.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + } + + // Check that src strings are non-empty. + foreach ( $srcs as $src ) { + if ( ! $src ) { + return new WP_Error( + 'rest_invalid_param', + __( 'Font face src values must be non-empty strings.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + } + return true; } /** * Retrieves a collection of font faces within the parent font family. * - * @since 4.7.0 + * @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 ) { - $parent = $this->get_parent( $request['parent'] ); - if ( is_wp_error( $parent ) ) { - return $parent; + $font_family = $this->get_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; } return parent::get_items( $request ); @@ -201,7 +203,7 @@ public function get_items( $request ) { /** * Retrieves a single font face for within parent font family. * - * @since 4.7.0 + * @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. @@ -212,23 +214,87 @@ public function get_item( $request ) { return $post; } - $parent = $this->get_parent( $request['parent'] ); - if ( is_wp_error( $parent ) ) { - return $parent; + $font_family = $this->get_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; } - $font_face = parent::get_item( $request ); + $response = parent::get_item( $request ); - if ( (int) $parent->ID !== (int) $font_face->data['parent'] ) { + if ( (int) $font_family->ID !== (int) $response->data['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"', 'gutenberg' ), $parent->ID ), + sprintf( __( 'The font face does not belong to the specified font family with id of "%d"', 'gutenberg' ), $font_family->ID ), array( 'status' => 404 ) ); } - return $font_face; + return $response; + } + + /** + * 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 ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. + $settings = json_decode( $request->get_param( 'font_face_settings' ), true ); + $file_params = $request->get_file_params(); + + // 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 ( isset( $font_file['error'] ) ) { + return new WP_Error( + 'rest_font_upload_unknown_error', + $font_file['error'], + array( 'status' => 500 ) + ); + } + + $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; } /** @@ -256,28 +322,27 @@ public function delete_item( $request ) { } /** - * Prepares links for the request. + * Prepares a single font face output for response. * * @since 6.5.0 * - * @param WP_Post $post Post object. - * @return array Links for the given post. + * @param WP_Post $item Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. */ - protected function prepare_links( $post ) { - // Entity meta. - $links = array( - 'self' => array( - 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base . '/' . $post->ID ), - ), - 'collection' => array( - 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base ), - ), - 'parent' => array( - 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent ), - ), - ); + public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + $data = array(); - return $links; + $data['id'] = $item->ID; + $data['theme_json_version'] = 2; + $data['parent'] = $item->post_parent; + $data['font_face_settings'] = json_decode( $item->post_content, true ); + + $response = rest_ensure_response( $data ); + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + + return $response; } /** @@ -317,104 +382,101 @@ public function get_item_schema() { // Font face settings come directly from theme.json schema // See https://schemas.wp.org/trunk/theme.json 'font_face_settings' => array( - 'description' => 'Array of font-face declarations.', - 'type' => 'array', - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'fontFamily' => array( - 'description' => 'CSS font-family value.', - 'type' => 'string', - 'default' => '', - ), - 'fontStyle' => array( - 'description' => 'CSS font-style value.', - 'type' => 'string', - 'default' => 'normal', - ), - 'fontWeight' => array( - 'description' => 'List of available font weights, separated by a space.', - 'default' => '400', - 'oneOf' => array( - array( - 'type' => 'string', - ), - array( - 'type' => 'integer', - ), + 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'fontFamily' => array( + 'description' => 'CSS font-family value.', + 'type' => 'string', + 'default' => '', + ), + 'fontStyle' => array( + 'description' => 'CSS font-style value.', + 'type' => 'string', + 'default' => 'normal', + ), + 'fontWeight' => array( + 'description' => 'List of available font weights, separated by a space.', + 'default' => '400', + 'oneOf' => array( + array( + 'type' => 'string', ), - ), - 'fontDisplay' => array( - 'description' => 'CSS font-display value.', - 'type' => 'string', - 'default' => 'fallback', - 'enum' => array( - 'auto', - 'block', - 'fallback', - 'swap', - 'optional', + array( + 'type' => 'integer', ), ), - 'src' => array( - 'description' => 'Paths or URLs to the font files.', - 'oneOf' => array( - array( + ), + 'fontDisplay' => array( + 'description' => 'CSS font-display value.', + 'type' => 'string', + 'default' => 'fallback', + 'enum' => array( + 'auto', + 'block', + 'fallback', + 'swap', + 'optional', + ), + ), + 'src' => array( + 'description' => 'Paths or URLs to the font files.', + 'oneOf' => array( + array( + 'type' => 'string', + ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'string', ), - array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), ), - 'default' => array(), - ), - 'fontStretch' => array( - 'description' => 'CSS font-stretch value.', - 'type' => 'string', - ), - 'ascentOverride' => array( - 'description' => 'CSS ascent-override value.', - 'type' => 'string', - ), - 'descentOverride' => array( - 'description' => 'CSS descent-override value.', - 'type' => 'string', - ), - 'fontVariant' => array( - 'description' => 'CSS font-variant value.', - 'type' => 'string', - ), - 'fontFeatureSettings' => array( - 'description' => 'CSS font-feature-settings value.', - 'type' => 'string', - ), - 'fontVariationSettings' => array( - 'description' => 'CSS font-variation-settings value.', - 'type' => 'string', - ), - 'lineGapOverride' => array( - 'description' => 'CSS line-gap-override value.', - 'type' => 'string', - ), - 'sizeAdjust' => array( - 'description' => 'CSS size-adjust value.', - 'type' => 'string', - ), - 'unicodeRange' => array( - 'description' => 'CSS unicode-range value.', - 'type' => 'string', - ), - 'preview' => array( - 'description' => 'URL to a preview image of the font face.', - 'type' => 'string', ), + 'default' => array(), + ), + 'fontStretch' => array( + 'description' => 'CSS font-stretch value.', + 'type' => 'string', + ), + 'ascentOverride' => array( + 'description' => 'CSS ascent-override value.', + 'type' => 'string', + ), + 'descentOverride' => array( + 'description' => 'CSS descent-override value.', + 'type' => 'string', + ), + 'fontVariant' => array( + 'description' => 'CSS font-variant value.', + 'type' => 'string', + ), + 'fontFeatureSettings' => array( + 'description' => 'CSS font-feature-settings value.', + 'type' => 'string', + ), + 'fontVariationSettings' => array( + 'description' => 'CSS font-variation-settings value.', + 'type' => 'string', + ), + 'lineGapOverride' => array( + 'description' => 'CSS line-gap-override value.', + 'type' => 'string', + ), + 'sizeAdjust' => array( + 'description' => 'CSS size-adjust value.', + 'type' => 'string', + ), + 'unicodeRange' => array( + 'description' => 'CSS unicode-range value.', + 'type' => 'string', + ), + 'preview' => array( + 'description' => 'URL to a preview image of the font face.', + 'type' => 'string', ), - 'required' => array( 'fontFamily', 'src' ), - 'additionalProperties' => false, ), + 'required' => array( 'fontFamily', 'src' ), + 'additionalProperties' => false, ), ), ); @@ -458,4 +520,155 @@ public function get_collection_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'], + // 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.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'validate_create_font_face_settings' ), + ), + ); + } + + /** + * Allow the font face post type to be managed through the REST API. + * + * @since 6.5.0 + * + * @param WP_Post_Type|string $post_type Post type name or object. + * @return bool Whether the post type is allowed in REST. + */ + protected function check_is_post_type_allowed( $post_type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + return true; + } + + /** + * 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_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 ) + || $this->parent_post_type !== $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. + $links = array( + 'self' => array( + 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base . '/' . $post->ID ), + ), + 'collection' => array( + 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base ), + ), + 'parent' => array( + 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent ), + ), + ); + + return $links; + } + + protected function prepare_item_for_database( $request ) { + $prepared_post = new stdClass(); + + // Settings have already been decoded and processed by create_item(). + $settings = $request->get_param( 'font_face_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 = $settings['fontFamily']; + $prepared_post->post_name = sanitize_title( $settings['fontFamily'] ); + $prepared_post->post_content = wp_json_encode( $settings ); + + return $prepared_post; + } + + protected function handle_font_file_upload( $file ) { + add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_dir', 'wp_get_font_dir' ); + + $overrides = array( + // 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, + ); + + $uploaded_file = wp_handle_upload( $file, $overrides ); + + remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + + return $uploaded_file; + } + + /** + * Returns relative path to an uploaded font file. + * + * The path is relative to the current fonts dir. + * + * @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; + } } diff --git a/phpunit/data/fonts/OpenSans-Regular.otf b/phpunit/data/fonts/OpenSans-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..8db0f64c67ddd51c0ca97a37b4b791bcd9c2ae19 GIT binary patch literal 254960 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
  • ?p@wz#0(@#h4_2bKBkZ5!)1&Ck+fnn8 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 literal 0 HcmV?d00001 diff --git a/phpunit/data/fonts/OpenSans-Regular.ttf b/phpunit/data/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ae716936e9e4c1d23bd12f7d7e44fdc695627a5d GIT binary patch literal 130976 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

    {}108{>zpB?RT(wh7B9kpej@qbWYDvK1_V# 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 literal 0 HcmV?d00001 diff --git a/phpunit/data/fonts/OpenSans-Regular.woff b/phpunit/data/fonts/OpenSans-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..bd0f824b207d6007f2164390dc9df31271c42119 GIT binary patch literal 63712 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 Op2f|bD7mmB;r|73w5qNE literal 0 HcmV?d00001 diff --git a/phpunit/data/fonts/OpenSans-Regular.woff2 b/phpunit/data/fonts/OpenSans-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f778f9c8455f265d05e653f5de4f4979fac4b4f3 GIT binary patch literal 47016 zcmV({K+?Z=Pew8T0RR910Jo?B5&!@I0iOf_0JlH@0U4SA00000000000000000000 z0000#Mn+Uk92y`SARL7X24Db)eh5Sfgk=#33Wc;3gzHxUHUcCAojePUWB>#p1%o~Z zzHkhJd|NAWkwD??ZzfW@6P%l=^;+ImHEfq{=uXR`DrlX1D`aQS0J9l*+f13!nzLS`uoZlY8d8yfSRC_(B%ySVz6II}HPHdigWhWoh> z-1sT3Q$Mr##qOGl|8=vJ4vqKJJc~|@0b|tdxUe|`KP0;aSON2;*5V|g^G2`J^uY4S>afdmwP2k zuv_5~3~0%6EhwLd{&AXr=Z!3H+4T~6(;lS&Odp6aiQXT4oA&s82ytjj$lqGLfEa>A zk)SceMb>f*9_OMV)>0Y?ApVGie1Q;3$z{&5ZfXzfq%0oeP<#DN3AyZHbN;aAa_#Lh z=CbxMm($vN0Q9zDjNe5Up`*M?Qljr9zucRWdm1^L4I5*d8D?h3nPJa4w-|Z9r1TDq zF?2%D?ta{_oePNwC2DB?WPfkSPOJpP+KE`BG*TfI=5wz92Lq_9zzm=)7zR-n1sog< zVBXDcxM{<|n_mja_kU6Srs0QJ{oKdgg;X@}+_g3JFPZYCD@eXKm+xp@)YwJFk>+po z@|<+bw9)Ncg7XK!kz^<=It>8P?~msfgZWTCczvOjrMgi1n#uso&8QJdq~wSZ8?iCy z0c!?QV;i+K3>dM7!Kjvq9;u)pUAQ|6)otaBIYZ7<^TPuGqB1k<;ev!voQ7Ip7y5(*!Ck7R($>d^ zuaslF>tt5<-V4Kf!=d4~O-o%fzt3R|u3?~IfH7;_-#xqBH77%mC{mIrh!iVVDUDY) z2w&-_&Hk%`CWI;)!gsy?%i*s%&>_rwJV|^_Li!SBR!3G{yPI!vIW6?2KZ_E!_2>GI zJK*UlsQ7T7d@%rTwfL6lykz!Ui;xsXps@z0kczur3#X;7OSV_i%KiV->ZG30vQX9t z2*OH-cmNDyw#b`GC5$`Spf7-cf!n|>_1!6ef!}HRQf$Cy?`5vQzQ=p&`nA8jc}{_x3h#+ZQF%wZQA zK}pu1lH&7Y`}-tL;>_7S?<-Qbpvy{IR9alvQwqw1{fS_GmLj)mlLUI(hxK1zFt7m^ zrzJ|0d1F-dyEzk^5csry+xLCWsoH(d?5i0hG-fD_h=@obg%Kmo{m<*yoUdM>fcr{A zY&&+00T+ht4wq^7{!||hcxDf~Hf#wA5L$N{ar@Vw*!K0`pkm}=B9X!rVFJRSy?4zi zPE)4t0Wc+6U~$Oy9k2Jk&U#|6ZU5SQP;L^w;l0u0Puyj z>|^4yTOU{gP|!fW6%gw2q}|t+Aq(w_b2~eLMNO3-Jaj{&&jxZJM;=`*fSlv%YMvBl z^26?QCs#78!F)Zj zM$Ue_sz67hQ0EPSQ#*htWtLEz1(f{#pe5{ld`OmZ(ZGkUM(YYoLlb- zvODP$-}=EqEJFKnP(J6;4k0)g`ID7dmDP&>FELa~nsLh=h6sbXsR&kqI*WuWESCz@*NT50T@HG3@x8rA3k{>1gr z#POV-<-h$uj(Ae4r|_gnml2z;UtfyEWz?Zr*R5yAz=p}NLQdX-u0mbJM)S{K0%LhwO=__+(Kf@b(*qiho=VP7Ij*=YC@qF8)4i2q~ReP^`FzbJ(-RSGo zjLN8r8puT+8lo}UVG0XMCs8@7&_ONgc^&Chy`i`DxX)UFOtn)ROpT|OO|6>ReCm)D zn^xFFmq+kh(^qq2N=-!S5l_4tw*N0n&~9|v5c)v%%q-0@5CDB-m8`v1-IMjwn`;n^ zCW|MNx?4}So9v2qqC?GkgZ}AFZm4TTA+z6|`U{^T?@iq6zL#*1Pp78$`F&=O=o5do z%g)8{$BIF_@nkxi10Vz=D25XxMKdhN z3!)?|s-_#JWjn6t2Rl8Cpd{Um!Z=N`qAKgAYx@yZqgK5-EF5f&8Z>Fn+qSW?X-lo` z1_arX&-235taXuTX*g zI3b?pj*Zb2&)5;BXpRm2;W?x#hAA5lzg^N~K*}*^lpCLbJ+FOo6(WDgPKG+2`cv(9EIq? z`^gbU=eUCZd9*_NtG`B!<*$sfv_aX6=3CGWE7fTJPyz4_$EA1jYo+NL`t`~LKyYe- zBI;Z&qobVSZ3F#!+|>g8ifALr^(Q79c=({PVxueJSo?nKqe}MI;|0TbdOr=V>b)bIg@{<(a0(1C zoRV!3kZ@+dbl;-Eq(98l$2A?JKzqLCSSSQ6Bq!&SOE1jeQ~BE#Z*8e$lW==Yq-bch zdVqWz0K@Y@`4re<9@uv~fW^UY++Z8+9*?X#Nl|M-vWUw*-mn~aXJ?8j%QB2RR)YIp#9 zazX2{h0H~(GcO{RwrTZ^Ps1ED@fM;*549xA+P7WNMtE^e2RNZqA{!Dd=t7ASNU1|8 zWQB>N-G{3VTC&dLrj3P0~ zq-wN@<_{~XLRxur%T|qaMD6b-nN=0BqOAd1wmVZ1`k?}Lt`tQnLC&olUzL8-)tM-j zqI8oEX{i!pnty)>NhC;>=vB=+pjE+)}&>e^f1p=`fL~rlZ(fGH%u}lxLo?0vJ6c9*n(P}FIPhkB)K97yuds8 zKl1j~jhBh%P*d9TFWG%$_tUJV^)1%U8eBV_8Kq2b&aw#yEQ`$P?j6x+heuC7$DmBGYNoA_U+o*;9Qy-X7l`&g1?} z>0@?v8Gx&zq(D+hK??^7CFRx(Em~F;!#nW$+-AjA<`hz=m%ho_5XuzdMXB8Ojnlz` z5TY1GX~#H1nlzAT7SdNF#B!qt8JdpRmw5NIPAR(t&}KGmorOAUUx@;xI#d8e6FqN9 z^8syw>NUzbHs!G?#3oeZs+prg8*t*Hip0oZP428`LkEO_kWyOvV0N8HWg4C za*bzO_l^}&->PW5x;WwS<>Eq>PsU~4p!C_35PRfT{c+-YQ<2u2K}n=gLSUDJEOKsa z++lrVo3-W%E5G2DqK?13pBNXt2qh{>yDFMO3TC=X&sttkHRzQ}X)xHpQBHKIdB8w| zEO}+>cTDCnGQc0ua;R)lNXA=hJZPpVk5&sAa1xZ)@L0b=)A2qSuq7TdSk8;;ZeYf~ zcB=c?2~?e?y`hG83Kp$*fg^*9K4OHX=g5hHr?v?TyXwFaHEdg`F@s%jRH0tSTF?VK zg^Ym6hHTTtT6=^^pPm*P*H}<=tQ1_2Ni&AM@K2+eXhs!uUWmcy?7nI%y4o3}*4)x0 z88>pGVi&QNXX6Q<`(*Dh($~^VWyhqqG+jGInS9u(S?@SUYB=Oo%zMXKe#&R7S?lt` zI)lVZArUF$Ln1&rlvbg9wk2VP4`7i6*itYOuHWVp4+$!%U+B5=P5k=H-k2l9qHNP0 zDeRcGv?B{sz8inkIu&Yx*U<{NU|X_VnJ`qnOTp855dmu^(C`u3etthHnB_NgX+`V! zDy{EC#Axk@4AUh*2xi?rhE9)HLfn#WLLS%?CMe5X&#g5*zf?qK*PhwV#S08=i8pIr zZFt$zda2VzTR{P%PAoUql}by-MG0nDTu2?GY~!Zztk%msgONtI;c*}r-O*>tdgOz3 zQK+=qgO%k*aLwMHxPO#?GFr-;rw*8B2UNH}|7+CQYwKb#n(pE6G+dTGw+!(qAPxbN znHrQ>bo70fHZSm~@Tf4^u96TBC@i|fKYek^32jQqzp>(v+#~#e>mOeUfoCH4+7~<( zDJd~}8ER<)KFdg0klf^A--Pv`0dEikcVN;Gzz zg4Yo}D#eC2g3Vsn!skV2Hmd`!oK!O&u8pN9*K^kJ=h||OdJoolE#28P9Qsu9&VLun zrvocZ%@#8&nol(WOU7_+{{h8n&01-$AHK#usY#b`WWaj@8@7T07aQipcQm&}Uy;!k zT_3Y}veU<>P6{=s-iRLmP0)LeQ_NgxLPZTJoZB1?kimxMXCw*U8zmrg4O*{^kbPb$ zAK}zN8E94P+d&n%1*phN3jShVDyM>rvEkB)MYxfih*4tDTJV6Pd)!tILt8__tipJr zLwtA~8YAV)?sH7e9N!&69hi*q!3Wo&sPa?Q=zvMX`+AI;wt9Z;vJ%4nnjfQ=t}d~{ zPWmym%$GiDxva!RF`ec0b4P=j(7vs|PfgW72&LIvP|<`ue=bh#@JfI+k?7^pB`Ppb z5z0q$aQ+6N!1XVc{}TLpgZR6|3`Sn7G>bz~^)=G-#`RIcla@5K6#iKgOG{kt|6mT3 z6<5`LkH5?V3P8$wG}>Cmi@BP?^z!r37j!8dul7k96I3$7?r)CF|DH_LmO#O8J-U;#!-s+P{RNu&>LG|&0CEMS^Sy|Gus&Xbp8t{ z0Qe@i6562;f-MGH{>cw|5XLSzbJ+UJCbcjS7Zft0qDPR(ID|D5InB)Ou4-g9cTQ0A zL%UkN5@LM@B2C38eT~m`Tpjh|6{)~Y9GPH$t9qIXLS=g6_nBrQnsCx`^QV+lz23+a zjX+C$PyoYeK z!WPlHMDqJx&RGEJS?DmntGTpM+pgrRoR;SBWm=xynBmv*lEgmU`L>>l^!--wN7w<% zsZ_U(73A-bJpRu(xLETsVI}O9t0D6v4wM+v&wfo2vR_w=uqXSNf`g%V>?v>A!%^F& zD_LLpt~zNMH|vK>%GEN1rb`JcGF(isHO&Hq2IriBHKb$tM$1iVpS?Xr9xU;T65K02 z@|VSkCS9Yt)P1N(LV^x1oY3(#pe&v=81A2BBj7#iUih*96jB)F>qm15Rd<{l+CW-bS>AxYRtWw=@jxXQ49CMeDi^#0-aixApneA9)!Q27fiv-Dlel< z1nhzu=J-+sdPDyC(1Q*4%JiXC-pxP)fD=T$9zGp4Phol{nD#qc_@XD9A|0T?&Tu)W zHmiXHPBM5^HkzOuZVTbcy&#^Q5Td?&ogUSdKteqAgS8gb4DoNOq7MEBOP>J;YQh1{ z*XMc9N>ry0W=qSroH{^kdk0OJ;8L^hMLUI9?E5$L{kTH8ry^RmnrXN4hzldo<)G8B z2T=)m=4n(K2ml!Bc|BaUEE|)tDD22o9y7BrbrL|9iBJEg3(`B}y*HwB;X%*es64r} zTbBy<_efEabOC?mYyErg<2F>w@E>{JKbw;)nlt zY0H~O8+m$@MzQ$^U*E`Y2%-em8$j>a>z(rlpkqtI*Rz8OBG`ach`tT}-(@Z7IDeoL zdyKREI~QZRQ1jOKHeAOGl98ai7B0gP4cA1<=olRjePG@?N&)4*OS^8FCJxwiJ8j9b zkcW%}KsDDrKqst-H1F0|U4p6WG?X*;uzLQ^g;>04(~B=Is+bmojNS249~L!m*zei$ z(_B13MsODgb*S4hSa%L0xRPf*4r(DH94lJv9HkHs6pMu(P^gn(nheQQ5e)#iS}Opy zq*Ub|&_~^Hg>u;xwM#**U}}*6d`6T8CuTNEqrv^bdE0-=11DuQ8+)*3k2MJj zXsKlxKZUB9AQ1RNPdn17k=Q2JV%o$)%ZK76v*$b5lXY&7>gBteoY2PwuljWI9Iu3_ zwK1S(ZWvv7uVWD!>)C?q?;Rh`K2As+My0#T8ak-%-;6~bl#5l?0?t*B#yzKS8zV#Y zlkDtEv9Gi|B8Dz>tGi_I6n9ZXewvIX*5f{{mThSVCK2h-W8iTo+gYF7sZJh-uTmPu zogCJT?=t6p*daGQY~@#`c9MduwWd$q(SL2&+meM@57v5D?{luE;>+3+vW@uwm>uA% zBvz<;P@{hGbiK5<^Qq45YxAu=1VFV-il)K>`g0*2bGhm7rcdwJ9K6Cm^312&jL|!- zkMGv(wG-}41!v6HOK<)sWjUpe2a&lP#|l2fUZ4tl6~FRmz%vLO0z3CZs?#mR|L~_T zpz>$FHX)J~>RKs{B=0=0O0`>Gxs=VxAg7WZ+=!hk$oO~XW7?e-^-IL8LN5yPeHov& zLtiHC4GG6f>SBE<_?=>WF~9UtWv#nS;=`vNqb>D#KZq=QI+j;r~Iv z&vg_42Arq4#8NJ3@-8S5we>Xb3)&god4(3qS(pm2 zX*rUsj(E@@2k4Mf95XetpOPCgZ!Cz}vKB1{HfxN?OB`=`zRKR{_?yW~B4<`C$WJ7L z`HGn>p`1lziDDb9GrShp)5>R9oVjYQJ@W7Pzk5t!MROp(6is%Lfjo(86GhFqAis@o z>^I2}cyLULZ7f|#bEtf%q0y=FJQ`1U%;0YXJ+ux&jJ0nNpz$}cD{CsSsL53Vq}QX& zn_gsHIoSd+l#L9EdEA_zb8l=^wD74&HppcDJb}Qu9+Xi<3XuP6FWtQBTn$$LCHi5$ zzeguAezp&Twsb)(--IBqQKx#6uxJ}Do35nN@LNL`J$+~|6#Uho2*V9;q~ zcJ5wrDwzas9fkQlZ)_SG!SqnnI+ck{!9fCoBjhosTN<;3r;`l=!p+5N7Az!no3hsv z_r{NwdwIIM6aNOV-kRJOM^^wCK?`h%umPsKxFqO7xM}=j31qKn9DRD@n!7Cr~%0{v8 zZPowskR>IqlYArPGOaiKVl6{iy*T^pPoE_Jx6S@C*Ro5-PJh%_I1;0u%}tE5+czY{ zt5hvadVZs(pk{ql3xlf0+>cKQDuRRJ|FPkoY^!UcfK)szzRMmxI~e|GR1;ro!~F`x zTFk@nOFY@PTlR5?UB1JuGJPjoT$BqgeGDF~X#=?L_3e&gFUp{`0&~L9P?xJ7ORYB< zaq+%d1RtpKHFc3fwk|dc!%LRb2f-Gv6x+PvJ!#O{lSKZh%#}qQo0P!N3qYg(2 zZ$1S&=_}*$;Uk&F6cu?@Hnn)c6gvxPGjs%cU{8OCLkXW*HmTvJ?CCm_Bi*-og{v(r zlln#u9wM=9;&Sh}Wv?6Mej|gnqFHdMC0%bv>kI#w>dO|Y<+*Rf9~EAxs8aE29iO}r zTBeZgR2-Babera{o~_3X3kCwhOA@O&Qsxy5IVViSEZyw%vY%4dPjs^%yw)z_5T|+RCn#M zqETV6(T5Nz6-JK~2K`}B5j6HIuuJp&nxRVaXpGuQpBvBZ#x{d1&W%nsD9>4bs29Xc z^+LKUKiH;EY-PRvUs^b&_1$9VfZVvA+8ME)g=O5z*i7ppw=wo4<5D#nUKLXhvD&Po zD~D$&pS_%c7l#`&`TX-F5S)8N)i8vU8YO%$ZG+XdyWkI7ojB!_P2xUC<7>srMknzH zsF8Adk&(T9QyyV^zE1WU-EMcRagLAy7lGIrw`NFwy>6zW5>v>B1!(M|zNapMPYcby zm#+MA7HRxkdeiRTmdkXS@Npv@MRmR^*NWz>a@du*Y?*$Y&WyOFQL@8 z)SvdTy^SxuQ zG*BeZUp5u9cq_--9`@_M8ItDHll#lb2Ug4L*g~{>@QO~oK5ZsmYk(0hPpsQyu#Ube zUkDmg66wf$ltksbRC4sVPR0|Nw@=~-iHW5Uwnmk}O70p8S@232w89s4DERhzPC=SzZil$m!qvl075`O1)!suqF_^=k!BpO@X@(7TJ8hKTlue zjqn!f@9!)5!uQ}RGAi&eJS~G}6{2a8j)dt_Suf;0*#K`5h;$pXgHh_?VG!7WKqMA} zLg517=nxNMdy`d#`%9~2dzbJb_{;7$NMPYV;oshWO_&yicG*uVa#e$ZIvo|0Xa%g! z*^HzMjY^C-@nD5!=)K?@^Ji0A%95&UUiSF4kaC;q5~$-rDbWeSjq=^$t#M{Xf%KaZ z16{zo{>SvdVTQ*|s9T|{_IP#{ir$l#8qi))!kUF=<4A%e{A-J(?VWZS9AA@l60mq= zb{RFMxTq7>s^kNXX@jIZU`+Az`4&a7z;3%2Euytx4dyVbDsU9%cE!tk>2z74wv0J! zMe+5WzlAk+Tos#&UxHVARuq6ge~yz17o+D0BBWtUqGApWEs+-D?G8Br~dpT_Hv2}7q$+%{tYXoOTTGl)SJmxgd?vT zyLz{{r03Q9QAYd8U58^=e@xG9{~j!5M=7fF(PbGi?1ApK2f3lu{px#Yxs}7Wn}hP| zY24Pl(&8o=Ph(`Ht0<(!z8u5aAIc=73#jnc8~EK-hR zZ+Jo}o=4o@?#*l&VEfP9fFrdLg|+9I`Ym>UmCm)a$gX$-5jfxU9uC6bQJ3apNrMRa zO5Ro|5vpg`7Y=?b{e8aX`v=JIuHO5f0{|ZPJIdK=nP{LJJMnjOigQvaNDXUqepYU@@X-N}6VrU>E}pPQ~73wDn0 zWT$n{3^E3H{Fl2;)*4OpRx&ZFB)Cv#vz+8a1M+1qRvJ*jawoZH-Bzr+UgI zk6}^W`>>eRvWy;v2w8M@mGvZH<>hvt?d`5})@&WMU<(%@%)GZ%>2w(8eNU+JTM!{q zv?qT+Us|9FpB8sx2rvvPSP+fk$nOshMWv+4L@-LUxn;D@CE2|%#W#7AeWo!^4nY@< zJA2dgp#-5B)&0uL0y4g^z$d*|e>N#lMU-p;pT_O1Kdr70*nyoS+N+a**5rBcag8>i zU<1RQVMT5RwrhPE083BR8N?Mm^meVo2C-XMOimjD!`QRz?fPB&)Bbm@3(o&(e%`-{ zd*oLp)^YBk`mRMq=(WD(K4b~93DMVlXV~%F^;j}=+mZMAgfSa*tObvh1PonOQsFkX z_PM*lSpRO-m1kxlX^F=kIQn}`CV_51{O0Cgg+?M+&BQkNCZXy=3H5K!*bK|uyJN+4 zzBWHJJ<^X=6saA?e5 zmreoz3buY@Ye3x*)dSbZ?fM+X0!EFWF0d6ou_%hl2U&Gep=qASO!_QVz4z&-6s#D) z%RT#nrwtL_n>#&IR^)P5XAVkk95#fXQ$14YIcV_oGNl8&_PdkY_zazcK6tkFRHaH= zdQw)B>IiVWq?wc%&Z){$zXeZdTs5?+cMsbVZC%f)evc`PlJKt-LQyH1RLt~K&Sgy~ z`K&4c1@$!vVx#A?Z>Z^H>Cm5IeWM=}`pRq*Ab5h1OprD2);+Rmdr!2oBF{&{_n%Oq zdqf)Q9Rl5h@a6LQ1#7R8Sjb73``O0I*7var0{}~(ae{ney}T0RkWJ?dEK#wZQyN{u zRnN7mqA7&!~`hNtOu_hK=28Y1Jy{`BB`%!O5qY&FC!Fnd9cSUKJJP z*mqGIul2?SB;{axF9gSDsZZRLl;=u6y#M|!ytw$HdESAyY$LUy(JRT)jo#+b0diMr zbY(>)q2aCmQ*DqaKniy0^7rp{0p}^L6&ooC0sP%qBTO`boba8zcMrmxF)+g-5MlHb zoec2P6oVI@a@cw28pdbB`jS9#0sVfQupoP^mI$>LT>$l%xui_xXf8mLawtW1c)fXi zfZUh-vLbJwqlW914J{jN<-IY8z?IgHT}u?5??Lg9@7by-;=VPo5=2$YxS2UGNe>?|?ZfQ!u0A?d@+d@O;zrFKTyqWOVPMVV-I_dE zILhsE%C#o%)A;n37ky95hG(Mh^+!9exRWaytSCr+@@R0ce;GL>i>v(Mq6Hp{`_t{; zEl5ap3Y{0{M%2wqT*%M|hr~3>C*+qYJx16c2n}xx$%J~db}jAL6^is?cat6Az#FLf zQ^~vF$*{;x4W@+}ps9OA*=%T$=`rAh?ZlBSzAM~OuOmK3oRn%t2i^uf-ahC8IBoT9EtY95 z{o_98FfRR3xPEmOcAjtoG2k920;R;m%3ng1pW>a%-4O2)5MZby%WL2kot3c$YCAji zP*tL#r(AucRH1TX6L5rz)5s`JO!@mG0E{AnF0x^3e01qFS8Mc06bQ#T|S0CqsXsmSm#Rq@V$9sM5|^7#rtL z68g)&LBpLX@$l;Qr6ZPkUuV#trPD|>vADjv7Eds z*iJPTky1$@5+Nt``5wY_bq6F98^c|CW7`|IFq1O^XK+!K8483-ow79&WeyJ6BnG z(QRR&>iph)=a%+Wo_E=|gsPU&;|OV#TM7>#ZeN*DxVa&6@Bos#GfA7 zi&smT^TB3Js)q?WS9f})dx4oEzv_x~zk;*QvLq_2k;&)Cxv9d9IxV)UKrl_Wl&E7# z>CVFf=|Dyv-n-i}p<9Zzq1!N|c(I{>n=~{zcCe_s^CDqp@+Yp=EtjQHCwILO8QV}s$Ed0_w zp)igl79pYG8E@9?Y>6!3ZeMH+Hq@I9!opAq75v^R6eK7%vAZ2wN#aCC0)>U`n8F)G zbq+p(#cixfz8@OnTymUkj zx%b%qx%`fy*cATvHlcAcYf!mvSG#r~`FY+`s)UAvKqmjs+ca#D-x* zmtqt4)BGlQO$jNjd|;munwUsrq$VIGblXa}Jfed|%I6^>%p?6%D~0mdE)d%@u;#35 zck>kY$fUv$h4(w7vnnVUTc84&TJ6ifck5)HwFS2KpD}^uQC0ur2P3E+b=|mpre$GX zd}Wt=bv^ZKvqvm4;H6q@UST?bd^shR#bI-pM<+I&d5+FzP#LKlI#a3C$9fI#%&L*a z@H$TP1(X~i-*kAUHp-&2`u2C#KGR6Wy53myC(M-nq$HL##KfhlV0ZnTzMQJ7hJ4OD zvUO`k{evwh^xDjFUnU9N1nnO*WgKA6^5%yZ@!4saAqhA?v>k93?;qvWz~ByYM^Dje z8=qnOUlgblZm~?`GIMYxM5de^isj+GBm6^?d}3?KrN)m|P|hA>pP7F}*yW=ykVGzP z9k+&;gu9&g_NbVA;==Gwm}Bx!ZivkXG-<;0sDt(-r%no}6UenIdS#Jt)<1QEL#@nn z%}h*eo2KAAhcvh%rO)^NErL&+&M(+A#R$5)E)06N>=`BT-~O#x3TKq{>a}Aq9g39> z5V#S4LrpWcBJ!I_h&6d3_s{c3C8lS~t60S9;@;MrS}NW$;mkWzn57wF!Pk2@Fklhx zz4dYH54&=+_J{5!}I1gZ+-kqJ<7ejw-2L>b+t4j4K@q(Bhpy9l`~pM$!7z5 zy-RNwu$Yx`^+`x8hs?o4@Us;-M!r_l#t+&L0i|m%qF1XHb7g2+v#$iU1j2yW*+l$d zGOsPMt<$FbL1@$#iFr*{!pA&K6O z$OH~omcB}Coxln%N4=+m@ExBcUT+F`=UlZBq>=-1|2S7~yIlIiUk~h0`-y-c0AU6` z*_V~#2(F5^4reFI<-_i$qwX67Gb6d8%k!plH!{wY)Ua%2V5TvWYm4B-WE6E-l$&m> z7gn+E{~H`l)!eEvxC1wjUq@STd8^wea1|B4PRvCW50B!-Pk&2UMZ zvRlDkCa#bLn>|g&N{aVx`CqQ%7Kl<(i=@ozbU!=novP|=kEzz>3!iJMq3W%VG*W9d zj`$r7@cDl2Iw}Ybp9z^>GZf?I@-}7umKWc4ORP0nnd>cslky6xSqeDb*0+QNzMHf} z*Lp{)saMHx^2J*^vARL&_v>m7&Bi)5(jJ}k{B#hj}T ze2{qRpS>fSP|GdNP$Xqn6_uOG5`3#F(o(G85dHH2d5I`ILtI|&5GdP)!nAZ;idSTC zi`@->zXP?aw>P9b_p_)a?4od)p;~A^tzvQ$3|^4_P!Q>2Z~BSBo?)tsOq$N_heY`M zquRS8(oq?2TNCD;k3nIxLm3Qkb`H`2am7=n>W)NINFCLK;uz%?<_Ls+{opB)A5_PH z5FMl42#)q%ko%!hQog)F&LJD@VUy^&Getu3P)zDlpC*l4QW8fd<#LO2nd~AN*>Iol zx`!aQk)AbnsYy5`4!lfJOX#n&XuBgy3j#sAgonUTtGY*y9QD`>#q44g?!91e%jB?* z5wNszr}NYGviKPX_&tjX8{4yYP96*MexhT^F*69y{!7yoWN=XA9qi<)>fyz5aDqN_ zmqX6jR3AGGMCTuHD^a!fW8SjasJ?0dBh&4khm*^f%U_C2<0AOKOP{~~(yB@A%I%^k zDS}pO6hW)2hbKm#d^=(f+=IV(IC&gUj(LE&fwS#t$iLj4)BQ5YAJGmpdP@dUYbeX4l5UnGWvI7*{V{xm4>LTU`w+jHR-y zZvrvZ?#JhvFD?Cy{?H=x*xu~!+!6Ywjyp3jh@Bj8Wyro;mem_ky_!NXd-&%EaBu!6 z;ESb-N6VEd$ss_)59gK&ib*~sT{CKJ`C*jhr?53K*;-4+zipWqYe3l%NKwS*-TZKl zt~H68AW+fmGtz7M-Bb;3HR?aI$KSuMzxy%5R`vG+M;+tm z7tn=;O{FzK&9s3r6f}0ADvN-pm!$xPjTYWMbbfu%2M~3x30(H)^NL+$ch5p^hx8Rz zC9CJyme8sn6}GyQfGKVvRdm;K%b2gk z3VlIS=e0A*D0Vw}$|=R8v(Dtf_#VYud?`flilDuB^%Fl*`rqz7$lKb}=z5@n6PQ~> zn0M^7sm-Q#l}sq&Mzj3+h~{0F92)Lg)x}$3lWYDu(uC{wd`bKAf zQPr~xK0R5WsqR(Nm?9?EV>32}J{2>K&`}8c8KowGQgs!HfBDIHjkcC+?6mIm3mCH| z!*FN^SYTuIUHwpMeo#WWPmk&HM+KCFrB=areUrG|R&fK%)^CrvYFSDWuKqPf5Eky5 z$)n@`{tISQ5gIKOSzEss7kSFUiN?aQL{;6}2K-#uP?=V#j^tj`;pEE|kIWNVm=Z42h1mA(S+wUPXh`L|gUrG&iU zu_qy#Hby@Gn-2jKyc^54#Y?mNwKToTX7drd4%>>%WVBBYnlb2uLw9(&c;tXs2AhuU zKTO10A+aAa!8K&rA^E#i2yRE@^!$%@>S#$3t&=5H$-VusXXGK!;c!F;=I>0#vqwW3 z?z)P25558P;4GDv7%}nIjvRZkupL`o{NZ=f@ke%PiZd;i+l{xBA>TJPewv8EE?eJ| z>=UK2kK!`Y?X!S=*G?}t_O6d{luRr#dSfk! zr5{n@;9?-4xpF|cP82ve86s#E2D4Ty&Zsr5XhajkL)|ZwH{Xyn<^~RTCHeP+PPo`A zKxdZuYzYA94HAIydV^H|`e2m&YsYfMExnJ}37DSV3~5FLEUK6(HW>6(+Quy09q|8I z*cLez;F8f?>^J6D#%rKw$!@jf>!@P!3x;j}8bg2j&$I5|t=BTFf57TL8>&=#Js|RXGAK0Yp|VcK$dTmidLm`?KRNHxQV!Kev2EK_p>_d!a;XAYPcPsu z#;d?9O@@8{1CL){{U4yS7;QH>)l7I5t<>lnnML&sgqGBHopza~j23?LdUMLgdchNs z?i`vhbfHq_udr{w_j=a3b9d_jokjO}-RpaMslrNs?F*RBrDru4A02Ou$!w$NX3y;X z7x<6xtxA+~Yn4dNwfrU`eCS;>Gpo5{yxNV@&7pGB3x$6W_?^4O!~k4Kg8^z>4tg<+P8!vVPhpzf$?|e@wW=Ll8TSTGl~fondH*5 z!e43oWGC6%H6t(r0-`=PS3uC2)sgo`EG7Gb`&4Gu#I@T3XwD(C@#6pE$L&p5E6ohv z50TweNFQ%?9oIc`mehH@N}4gGgom&uz}5!jwiR7Xc@7xCk8>FS!MK#P5lPjo!uCe6-o&9InzG2OHIh?uAG zdMe|UBXX7NS&he4L(3k5kCUcMB|aXVir)O8@n~eSIC%UhYg${88OaCxK#|%P+jf z2MG8z=p*RBZ&!u|W`AS-gQr);Z!TH}>=5+UF*(uf@AXtT9LkKjID2b+ zU%2h!);>zuLC5_1#(>oP@0%z{wps1rge|E%g4>aF(qDYt>aqI|q5?YNpBCtL#Tn1r zrRW&}HT6a?Lq}G30%t2qOc|$Bk))dX#zxt~DXmNnxf$wC=4+EZmW=MZe1LDfey?=_ z{Mvo<&ySj6@V=5!ueL2hr_}f%)K0%U9z5#RDq3^u-~y~#ViMUHB28~xGJ1?TlqKi$ z4Ye8$mK6*7{wSNi34__5NUDh)bZ^ zDHb@#q0EK`&ULwd<3UdS^?+WBgAK>lysM2O>b-Du-t-wDpHTn!>>4l#y@Z){lXXv( z4qoQ7=F}@y#_9Flkas_iss1?pZt~stgQwm1erT=cw;Uj^>#uG} zhBg`Q04YG$zx(KtnC&DBuhXFihbBaU7&$g;A-Zk=^3^UFYQlVEx07j_^}G;+wux6Y*)g)ZS0YisO*uJL!Qb^#7K6_7fN{jPm9QV>Wkg=j_c8NW-PcnCvQH8|3#W#rBvbX5l{ z;|z7*QLZRQt;tnyo>Ele+wJ4gz?-m-RT5okyaVW}G3_$Rm`%k)Prv;dc?ZxG-T_cs z-2N#h`HjDONqM4eri73?$sb2fm6XgK3{eT(Dmh*=8q>W&uirExuO6{EsBA5C{IHJr z(!Y+ve(T7@wx~_;byT>ye|^Wb@;R#~C;5zTn2@L3LxtEF8M=P68nUf}Ha2VLOwJOe zDd(FYfMe9Ua(JFzhs1yH|B_0KyqvB#5zrLKMGI|A8$G{|2tErL-}IM0JwsUVOl9NPyculT(ogXK&n@t z*MY+An?R{gXQ;)3#ds%CVFQN03g&|_KH6vAylJgV&xUcM@3NjNOBx$@7RU>B@Wi0d zgYcukXrVC%Etu^gS`BH?U7TyEYwabB@>JfgcMNMw@f-w&KNb9$wbzo@AtD2?{)}sT zf=z!I>LAc0xfE2HVx+jNDEnIXYtj;Hn+R*H*XB+rN~NiCO%{M7lsb}ro(bFW30`I^ z-iV8eedq610#>E05>#_n*G|rM((p92!mO!kXe4p#T9$Q+89Q_Wt!-^gxU49)TO3?E2Zv6q5@=UG1n**Y14lnpbYUp&{(4=v%pTBon!z!yJ*!UOG<6!~YV_}> zWTceshJ*SZrtE=DRCQLR_bLTqn5m#7OOYqOV^oe4l3~lQxhRT0DL;RdY^Ow$d68!Z zAvy;ZuNm#i`{Yk##%u632rtLwl-Ew+|+VcsS~}1sDo*A3~>s< z#(=t&zez6xh@T?WdmpB@xDC{HQlP!3d~uu%XQuhlg+_TsiZhA<%qscJ)Fz#^f_+O0 zQ`Yi}gV;Z(cw9AS^ZxTEvJ*7;nuI9NjQ%Eq@lyhD6w3&001hl>YHh&>QR@!RRhWCo z!r_nV$PngwrWQ}c+yWiRc|9z8aBajzFj(Fyg7hb6IP8Zr>R0Pm!T z@fyWus2xi2YS-dUm={Tp6)mfn2_APJNE+$PqpO)T+a)-BP#5|Z@1%yMu`LTD;0<+8 zbE?TG_?9r*)iW*na`f?r`hfN8y5P_HOSzxQ7X_Sv*+;)z)QSKn#TTgU6N>j~L6gk< z4yZwdoU(@_PSmuo--D{U+CP@9KmR5pp`#z$bwlRwzTo2GFQDynOGU#0hl(1Bgi^L> zb1pnbs=Szv#}`&uKhUqV4s&7Wl`?Hxa1TpBddEpUImje%v|*FPGbk#}f5o`U@*K-( z-ydF{I)*6#(_Er9^NM2uUY-mmyWc78QPl__SyJ^X6<<2XJ@mO#>*j!MVX^S7j;`nz z7;@;L7ys@%O*Pkh9@BRhFFkm7d+AeH)Yc(yz`WJhl_bVV@*nr=j}$)OD=;)enr5Gf z7|1SqsCA)APt7Q+sK6vk2jbQU@$hKWRNKXu6&dk$Ts-QRFMsUQY;@5hsQB`2C@WL%ir;4t+$NjKX3SY%V5piw%oWY3BNmX6X zYr7i~=TKXIv@rV7Yi5`7IhGCt1H2E{mQU>e`D{pShgAKj&bvJ$+!SEGn$@7A$tf#e zNscg3;E)HYa){O~I6!W4kb$`l5YJ_}esU;5=Hg@g7`kO;O!G%$0>*zmFSatij2K?sP^{ku8sZ zDjnsj+op>7!t3zX=7K96qj%=ZkeeN3uBi@?tTcn95JH}_zwuS*w5189u4BI(^cXI` zmKUF$W6-Y|RSp6T`n4UMFxDFR(uh?njOly!Pc+whP&JJ1$yqB(bKXO!W;J39qFV-8 z#>@j@X@{X{b{%}w<0NJ?`4d!??=lBqD#aeE{b4vUW_jBHZ)k8U;=B!V6t%6GurwQ5 z(f!3L+px9UAE?&&wmGxePp1vcowOh-g49OZ^kGw0WwO}HSRee&e8TQOO?Z=b#{Rj% zxp%Z4`qdcy`gZcP+&{rnES?ie44nSWQ(+1%-Gi;)%tfz--f}Qvo2gCrlha6XRmg)k z^U()UN)oXjS24b)=+&O^lW^TN-!n8M6khJCSG5%_k&h1S3?K?5{8ZawgS1Qt*C8L= z0HmWBqB7mbXL4jCJyyBmOo}w=upIamn}^UkjbKd_;4D9rJLxq-O7xo=y{U z)u|Sdhq2M7NbU!tZiYT>+PZ|_w`>jRLY=m#~q?IOw|nWgbSw(+3}r8`|TO`u%HE0K!0{DDz| zh_8x+LTz0FFYIkklBTC5X)z0R3TtvXTxup(;ETPxpGV_8>5d1UjKE(%G^L01(VloS z7b{=2mbft$2*eV~@n6Yb&qACnk&mj8vZMv-+yX(&>rnZjMkQ&)JsSpk(D>$!#E^>t zy0XC(Qs)V8i@Mc%@TQF7>29qocowl?F(K?gTN|Rk*TWW~V{S7OlM`bct2JaGC8lEU z12c*}#4JY%tkIA^#EU7L^-B9Pg?^;u{!b!ATvG}ZUK&4-1~4-HmI(jcCZFEzxu!I# zW6;<2kCI1$(x5g5&q@i;Q1JJ54yDP@$!j(xJ{XPrMuhA|3k=C|fkD>Es-!q$oMo>Z zT3`C}>;HdSlXNNrmk9mMUyO-5LHZgN&N@$3M%%oCx+Ecm)wO+73pFG4_rvw?6@g{y z&eOWOU;qO!Z|EfS_6lk_)0b1Hz{F_Am_8s@ z6icp;c!K27XX9Y>;v2ZG0hgZavLmdxxF|Cib#l(+b!A2{I>p+t z3H|{VNh*Ue(B5jzQu)u%YeJhB5;UZc_VJ_2mtM3)5Zp15M1KRF+ogL|xwXBqTi*Kxn&%Ekv5P(7NsDF{zQT~YOT-fy(7X2fph=Cb> zc2_NKi2o+dD59vBwOu3nt|W5`Ux*5UWNA0QMvnX4)jMiJSa{=Q_28fz09cP(dF54M z@hozays7B`Jmx`=UP$Ufs(Y_HoWAMEX9)+(jk6S4rss@QGoj?QZ*v6WX2zRVBMLm$ zC1?`3zc(TfgbncQyO+Gjbf9y1B!>u>&VySt@*w#7C64EERdt6wSQRp3>~_m|%*sqm z!7ItlqY=~%VEt`(W|h_C_S1^vt96Mtu0&m;s<4|zmg&QaA)EX+QSf>|mt+n?v#Lb+ z8)ag0?ymnMUmq=AqXvc)Kc`>r?8tm`&4W2ueo1ai)*-u2*rs!=0sp#S4+V=Sj+N)* zD2O(`T>&&N{D!gMjWpw_HZ4TD$Uef#H4<;;>MR%G9Cv66$$a+SI0Xj}ik$9SCk4_{ z40kv8hd($KsCDVr_}E9WEbSLKNYkyL^h}dDyNxdk?2*0avoU=Ebo1(21%O7i)yG>Melw9_f+z^zGq4U*Rq`G{F*KQezp!?p^vH zpGgD~$6aI+QF?2Le?>yokFeDumRQMN8x!uv#um8kBv z;#UZJCOTrpx~XM4eW0f$A`mfM@DhaVN>GoBHjK3HmP2bBzn>9Ua?}b}+G~JvM0e{% zYn4+#7ax&UoO^Dh$?G4-y`(eNTaTU_iqkb?*)HJ~8$NyH(_o`nKV|&z_@h7YbXYdS ztYM?BLWMTk-Gi3v1IRqQ3Q+uiK1lomsb2eDOU}BWQ`AThKA|zrUczsl8+UtQmP!$M zUBkqPqFpgqdh>yXzLtjMms?Aw1TpBEZR&1*GnbLn>_a}-4<7hEKK?^O`kS45+y9Hu z0Fy`dm;^FhIuf17?0BD!s;L76bnagLEY55xgqNnIU`gTyKJ={H2e4-s`Y_o=2}gXG zV`9@X{_Y$FJ6u0Cti53hOg^TwYJ0Jyv#FT$hT0z;a`tazvwz{qzq!I|!3i7b^Jfpd zeGR>zNGn!+63Mblc`f0uoP?S#Vg_m)Z+OrPuVcR(6Gawwt;HSDi$8kAlRUbPJ_b^iPdj+2D$a$umR1ZXw4tu25WjvU)VDK6!wDlJ~GJ_D)4J|NNXi zQaBg>*Sc4fod1HhSv5Swi5Mn&B?)K$T;3e59+FTjZOjwmdwy?&g^Gc?FVsge36UXW zU42c#@#BiH(d@eXyv)kF>ob^(J)KRELIY;AQ9<*tB|BAFn+_A+Ivd#t;ue`FNc14IvI^ZT;e zS|?GjpZArNr7!uWVN2E3WXR6r4jJp$?=2!NnZ-FIFHqxwug}g&TvuQ&@!C6Mq$7nm z@Py7r#7nz{@z16;4v}^hG)3LQf0$L>|HBGzWD(^6YjUR#vA*?uTMgffxEODP)kQofA?knlE>AU76arCsDG7gyXuv- zl{eVc7i#g3N8J52rZsuxb<}%JD<@P(5@_4~sg7NswY4_lbKMjKb0wY$kM_f5xAMjI zJJA>He&1&vSTSv^xV$Zffd-$#Y}$WuNSZ5(ZmmF^E9hj`U*L2g&sGp74}gZnhP9b_vz*-cIMVg z)(U(upmTJK89Fs_7>nH`l^#1un%lVM*5+K|Zt6R_)rxVG8?T7YX!iml&92rzceO?N z)lfn@1T2$Hf@yY5Ki z8K$Z`5ze2_3BuiR1PR$a+})psO&csODC*A5sOT_V3?CLv*1=GO#0m+5M2JtW&jccr z79DGOD9jE=vMTVz;A4ReKJ0*~AS^Z_C`HA4u#Q4X$5i)mrTG6Qt)--b^uxwDZF>nE zRoRPt1+yeG63Ak52s=$gBQhA78aEWNtQ`qr5ngR7zut+QBuC+6#mO0R8lJ6H&QLSx z$uh(CU|(E;cxx-feD5_~*4RJOTi#3lAx2)8PPnhoJgB1QL5IA!rkqEDkjN0A_{ua{ zk2}l1Bf@~c<5Me#x3monu6g&aXA_Zh1lx*6i52 zEo_8&>k9-U@1`7sDJQ9_IXL{iL2|?InX)Ypjc)%5k&r`LT`6x-UcS^x!((oDw;9g= ztsvp9^d<1559J7x?w4Q>OpCRN{bP6MorgV;8|5fb9ArpdK6k=ZzEZPc1v?i(<>HGd ztlSJLCl$ZTWbE!RnCJ*qk(sdw^e+reNDHqSg>i>8s=_TZi1NgJ2EQ{!rZ6>`c|~>9 zeOLQ${r^9;PnX^erexP=Wq|ezr1N0OFNYrXYN!S^?E8E1oX!qK+UHD+*5zcB@CvsM z`=jgPZ?%B^)aT;ZTsk#cDv?ABs5GMB0QJXnp0lho)h<1jY$JojCP&6MW$=rsPIjCW zM-n2!-No8yp5agii%E*&bY>T|sf&y?wyXDh)Lly?si%X-FZx|n2dlrTA`93)%CybA z;TxOYns%+B!}+uGl4~$u|8GA7jroj76)3xrsy1~$u5#5cwk*tr-G4h4Gqi_#EwuI7 z#>99WmAJP+n$DHZ)@r{T&3A93fg@Ct&=w&rkatDvIOT}zQ->rq$o+>l_g3(F=?xL7 zGj3SIm7DHV}-RzY?}Wcy<@;1+6_ev;t%I2@Wr#`s;ZG(6M^un~FpO$xdM2 z_0)1^a((pO*tgu2k}6m-ne)oImxq)DrS}RGE2k@7WmvO#M09qtPYR5cO(R66WLPG8 zv2y5PmpYkbN@_+FI-5$?A$IApJ^+&ZOl-`FdeE&US;Lnmd8H=f2zE_(Ku#w{V42_; z2~0M3_bEjs_M~R_Vfa=Fjs)vubC_>Qs6VZXm)(t%*b?oqR*YRRzcQ3lcS=q_F4s01 zNU|fFxdoJBUih{`ZA$cRKo8Z}6!~XAmxacc=}kdpD=dn9GoPvkK5Sbt>R9QRH2q$#@Rh0|vAo^bFdua^E1wz(0QjfmW4T+VrmY`1ayvW(J4h{oe6R`wI=I z104#@IRyYUks-5F@h6a7r}skoS3>d+oS4%)^Q(f)9sy+#_m7w-hu-nL3XKT!Az=aq z5kc2IwIN6(G>u3oPOLqQEi*slr~ct*g9l-FG=UI;3?pC&*f1k9XWo8pUYVn^5y!NZ zIAJ?hCGcQh2cjsQlc3GM!36!#yz{c0s*^&?f?_zHaKr-#63(B{K9s6cjeSz*b~4D^ z_>R##PBXws{x|Q|SQV|RwbJ)Djq-2)8a;`zGNJpu8_W>qxE0HSW5zB~dGmm$x*z%S zrUE%UatTH6o|0z>A~?ySR2Fjiw4710k6^(hE=R_+!>47MEWooOSf)%Fy#{FJ`$Upu zu#lou9y0@HAI{6X_o79L-llec%HQGxg8pP>`iOZCP$Z#^G&M*_d=_>VgSH zlt;t5E^k|3XMc_teA=$*QQ_v=o%Br}>KteMTy@(@yP~F56uvf;vt6Vh_Fl z*UsFYKLu%>Z;JUlU$2z!7{zU5A_}*L2Mc$y5ZKK-0yl0X*f%i7U~e@u?w}u`UjkjL zTh#~igz1ntRKlJ#S>gbK8lIGWV)}D8zdSFr{4h5W|Kdn<|08;V{c0=36Pkgqaxj!jGPb4h2`Rvpj(0J5DFN1Wxjv(i)0 z*7mIGQju3zK0vlF&^yK{4myhM(yRF3j)J(r=41zUm_tgLbL(%8a9op@;`VWjy!8U- z_6;22{iv1K2)$6?Yxx;@BPAao=8Jxga8q`D2C66Ibya`{Vk^e|zd~N$kQ^yjIBMK0p9=;r!GTKkhc(3&W#?lyHV@zQ(CXKhL>}JVCN*jm%C1RGF z;a?ur837R9r1aQTwg8sV7c39pT!42_z&B%6(gk}_@8P5IgfafZiW!<#%dj&WDdDwV z9+xI)2vipxT3f`7li``8DqK+{J*Uy5*M%^Pq~4BC)0hp=u5KCTSS9(qh&dp`UyfC= z3a<~o$K7nkSriFXw$u%C;Wb-DJwF~D^GNzq`v^KcUCEgGZa!!wF!$|TS&lnx=xy(; znvCLS6`mdOTS?3AU=;I)p_;;UeYDacDJEJI$9cMhOn!?KP^cX@H@C7u)1I((-i>4N zxGZ9ddJ6GnHv3Hy&dNrdtV7G~*JpAGrSGHv7Yl)#nmOGWxSKbSVsIbu-U{L%n}$ZZ z&*n)O1R9BdEpRn(rcb>;ra$>D%*lvc)CM>-7A+fIA;arq6WG|CW{W1In$LOz*1bPu zQCD-PyVRroNc{dl>B6gOglyO4f_i2we}B&f)a^LnIgK5*G)B)c#Ue9ZO&H_zLIsqD z8{6|_0eELNcC0(lth3R23+Nwr`F#-Q<)wFltM@r*ak>K z{9~b4sISA8AB;LVcfM|#oBktVTCUgl+w+W$HV2)813vtIT1l)O`LK(J_Wp1kc9G@7 z#eNtVw^lv_QI+LW=KxfY3we+aYd{VKPzVYrA35VIX#aPH*u}? zM$J9bIyJtaxsFL%)_yKh52nFqIxUA6eEn|L-HZLK>N5WV7UD$&EYUSRD71FtS#h#T z_nG+US~?EOKN-IPqc-b%D%G_bo}dTQ)U%sk;ys)3c~$)U{DAjw0B*~OzPoy#>smF? zbL9kNbh&-$p7w|P)>_uH>vp_ux3P!6EgON+Fmlc3VdaGz-?M^I_uepMYCJx!ewudY zBJ9Fn6Yz$_-7hKt0t=dulAcxZDSliJnK*&juUw?A+Lf>5`o?x&5^-pw(PH3vvqI-7e{MQXCXEYJ{7n}p ze^<*oheyY+I~+YJ_qMrCk|;%1Ha!Whm9O4~=ARGa8hSgO!Yc*CKI{l~B{tBp5U$dw}2+F!hR zp|IHGC2_#QBNmZC8}cve@`FIk^Z~lr^-L>w>qXlss_lSm6BUf zcAHtmjP?T}SGfriiLY}ZLS9HkmmgHes01(H9NX~s)7D(+5qDWcsrg|{{BRJiKiIR) zXo?_;!^&HX##F`0kCEvVEa(Da+mN1ZMb6d6%2!Y6U$QxO#kg{HHBX1<>f|y)KVgH!gIC z`oLM`?Ij)f#apogMCiS)SVf6@R4R(fXE#*+y68cjjG7|DKQ?d*LEYVPmv`ctwy#xj z!9(8aJ`CvhWsFx5U-}tf>i@>v|9shoz69>8>KbiFbeFWeRt3(PcR?}jboqS2ulV|T zaTqqK8oGSOVmqvE@3Q7Sbvl_hjyY-1ZM?3i(FtP&a3xze`!O8lNACW~Na?W`svrHe z_h0^>eYEdBr~Y;C?5kOFJ@(B@XMu;rgSV`_fS@e5`e`fi| z-~O&22YT?xqfce{K_mD7b%)=NztPiw|M0QjqM?#Lt4fgTiOUz{Yr0FCpu$i<3TKI% z#81kSqK9L`or`fb36#K#Usi#f|I7gAr6qhIm=fvhvD(rYYJCXEOKI*uOz?f5kCtG` zSBacvyj6f|^9Yb-{sBBOLds;?oy33YLM-OPZ9>!!0C$Aw$>hxl&Su>CmS76>wWKx> zd*92?BvODbH^#b_lxUVAU^NU@yfOo_$e%wj3E1bqV@Ge0YSEkZ(;g#0F+4L7MO`*)>@N+pvloRO@s18dq}$6IbNaF0{@FPN>u?qc-HB#0@Uy_9V}3Lt|Uwkc&-S z$#+^MbBP0wXAahq8lr06z7wLBN1k!{*?3bLcV}&(a!xyYsaD$^%zjLxCM6A+ElW*1cj2jJMs5D$P&VRNfUl*u_7aZobB5b zV%$5M6UYDDM5C7y04)BA`Fya^b!C=!ra3b)*TPAKdk2ZnGt*TdM(611)kq~ia5dPv zT)N;^b2e4bY&j$3XmRckF$mBWoM$IEC*<2E86pc<8|U0V0Npin{1Vo6JG35dRJgWW zn_;Em64zo_`JZ_nFJ0pA2EP1!<6%CJp+CNr;@3Q1#=uEUm-ou)EVivEbw>|_!)@`)O z(iky2mD`bIr|?UkV$f70ueQBs7DX(pi+CnO^GV8R@r4i*d`aH9$=q&&s0JMOOCF@}9aZt%6LW z?&wCBEVyt`aO_DkS{Z#GHd_o@5$6hT-^HprP zQ8mR{c3PplL_!3Br;^FJP!I+aoRz$ zQh8!4-LVjamsx9px?-?V z8`vhWo@_}RC&Y*vd{0?=#JtIBWZmL=@V%QD;p|+6pAgN-B-lu<%QnJ$m6$0{4-H+v z1NDTAG$i1^Dse1z7U5V@OUucPvlhW;H-y~fw4QN4(Ue(c((JNxB`R=t0mpZhkB%f~ z+#X8EikbF3T(u*V3Ff(wxo#=NWh7PH3PNRqQUL@2l>!g|tv3XNA{!8#yS*njazcrF z)8S~21aYh71d*75Snl&$m`B32gaicVBo{fDg>nlxmR+n|>kha+o}Zja_9SG*Oy|le z4u+gY-*?*4>^K#nxXV{}az^)Xsex5~D6g%I$Z&UnQY-+uzfWH4hpQe+;>?k= zl82HrZjYakO)#dv$(&FBMkW}}m;2tst#lW|XqJs@lK4hP91QU0uhZ zUNy@`D@i$OMt7bQR|)_N~)7{w!?kL1lyb4-^H! zyrqTYa?Wm!kck9!UoQ;&KL7W3|N4>0&$nJP|Nrs!HFffJJlA`k7*gVmuvGc6;X;`f zc^mT6I_;S19LLkd``O@x;E>EMfbJIr#l`Ltmkn8-fDh+ae4Qx%|A@W zd?;IGZX%!;#Ww55w)LkS>X!<;<>Kh#T^Rzw)5W;PCHBzqNwzd+3-rYb&)a}+lC9?u!iO*lj=Lulfn0KH&0tN>j}_^eb{>WJkhnm|L>}-UpWvwZ zGk%aank%2C(+4NTPw(gB_5OCcl;q$0$ET-9FP<~r8w!PQSP_bLy0X&}G3-yx^PBU} za5z5tls4dd=^Wl~UZ^wBB+X}s{4xBd@Wz#x?%h6m^L%eKa_^)NzMS#+naD`*$)B%d zeg%ZgXOCW@1orRm7Ke&<4e?_~U^-MI$0d4nZCeR|tUwP1RA_Sm!W4ilAp@cv!;rB7 z{#-*<6>&3?eCr?zuG<->?GOr>04+5$E*cFEXZQkfgLu*m>j=vZGXt)`f`I80T?By= z-Y`z1o7WJb2rJ+=@(dA9Fyi&WnS<8QJ~m~;(=5SpG%Pu$@4~R;;DVL35ME}0_DXp;x&sYB*vk{NCz!c|_%eQ@NS5^=1)ERChzb5OV;rE}z6Vp9aXl&3xHuyA zaTpL8R%^iAd_1ak*Np%(P$)LbGw;OT|L^C>f2iuuy{f+cPz&)m#Cb6>l!4tTAZR#84 zk(~!=@7laO1D>P~d9pSftTa~GvGVmWjuV~mEFH?2F-Qr#M6AthXz5b4Y2x0j_2v?; zHhCgWEi-*RRG|$X%AlCuCK%*W6hd8d8)3p*tfC1r-e|LQt8J?ko{urN=-8`Y@Al9x z9a+}id~-f`Um)W7Xb%rcHgG}55D(-KB5t<0V0-AA!S>dUXm$qqS^c6&^aE7$FC-nr z-&T9N{aHnd=O4JrJ~s7X=CAvmI={Uq4ygH$mq(qyAM@v^V}JVi?tb|ZZ`qf2Pri2| z@~!)~e|)z8Q8wh`{U^__q9aS!FDbAI+M^qhZYPk;Jhtj49Fd92hiZ@ShCM9T%E@js z^3q9W-Nr^(Z`?C^%4HfZ9o_4RE$$k*w34jZ{|eb@C{tEA0$8k>ohfqp?9_Xyv)dSw zb|aHPGN*yiyFdX#-V@w{PfsYgg*)P2Jz5?M8W+9N21S|TJcrm}Mo^rn;@luP1IP{| zS*G9;tH>0eVhd%b5?vUuE=p8w6V~?ojG66pjhBxX76g!^UmWWpAMs*-zszV3q}G!C zC@AGtl2(Tv9M|}=4#N3* zkI

    6a#@WD&~r_^5nemlL^2H!eo=da%@ChW|An1a{_QM2-Qd$HfYVD(nL3n1myY@ zBu+VX#1~xbSn+EWf&?Zl5kzcQwBHxr>DZwTtZhi1l9tM(0I8;?DIQ`+SFv7Dh|Lm4 z_{P;BDS#Don4Sz->TQ`#F_>0&-jhAtHKWJi3K>1VxLv_fj+;fVc7L#79oFIMuvd|> z?lEGOE$9*g>0`%YG*-s&$PyS8*D_6%(juaz1Og=7QA;q9H|pCs|bt zNia5~r}%b!DFzw;%tVL?V0}0{!>BTQsu>PF^MC*E*B%Z${cF$Q8NYsi8vW1oz=KZJ zf9}=vCC@)4vHf)EpU3vyuF2Q1jR((bd?LaBzuSkOJZdh4p4zC&!QtI)d-yk^H+}j! zz*Y^z5M2mPZxIN_A`0;+jaF3&r{hti6UqewB}aLLfH+JdG&A-jc+E(8U_vM~qAFP{ zMU-_BlO?$l`uZz?KovvG!Hl{IZAKd1;7beAh>ChW5}-v;qqkm;QFy*Y%IJC}4z2&A68dZW`|JHQx+fqRcRp;$t58?*w; zMk4sDyodBKVCVvXNkwa~N9-gA*+>_aKx32!=Jxv^GNEj>l3dG}8v)I)1JES-TfF`W zVbQ>B?uaNsqH5u8T?>@nEx9l{)hU*u8I3MeXu&dgfO`fdcGL|+5#tS?WA6d4!j;FlCSbp7OMkiS z8MM$u=k-Q0l+4fR%loCl9v=k(4DPDPXG;L(!1J;#9?no%Td zR9vC921NJdK0tyy=g>SIo4jnVql0NH^aQr#U8wSkQAzyj3rZzgC*cf}VmG&GaXQgT z`yt#;aQ1RA;s<0nR;eadGr7%n3AU?%I@IfCkxU!ThEzEEV0@7V@)8VlnOe5(S2U#a zIH9?!eV^pDgabD;8Wx@q5r)T`0P0|Vb$2B;o*0{5?^QF!gePLhSs9`e zQT2fK6)#BcI%_628Q6~@m6TaAUa>J4awr2}a0O)=XE&?WfI+WOWXh|G7~uZ!!SZ+hO-p1<;7WwQxLF^rN|qSId>TJ07+}WgaKR?rBk0^V{9&| zra_ZIMie%C_RhzZR~ubLkMXIe4aM|@1{wno6!C<}dVs3(47#)cAT+Wp8L#D*j#Ekt zt8);~f-B5f)~|nB_7)W~fc~t?4=*RnVOh#N{lD~B?k$9P8UcFy?^iuMf8G!1(a6)3 z`!L!va4n2_=F9f9V&`jz^X1*`{P54jY`*fb8}&5OI|K=%$!U)bJ!s-8kD$=%2Kx@S z15o6v+aOge^1q2t0k4efMeAZ^nh zr7+-_4#9)ibfu~~&DI}+Br>fMEm!2{H-68vRp5;vCkK3an#j}+c&-^-*U;pi$ikuU z1jv>NU(g6lwqdKlFPDA$(2QzU$%E8{z#wy747F9{;#tcx|Ihu?&vWS$)~m^j;>vi= z>sJ4JJ6k^*PX$0Op6iu9c^JLww>KfZ=Y-LmT`bTZP5kXT-&4gx52nTsfR>BTBeWM@ z=T+z~RQQxT9OvauhNFafyDFZbAV*EfP@|{V*=S6T`7y2FbRw`=>I6cWabTbgF_JZo z7UjA(YGOoK-9Ji+r6m0?m`NRof*5E+^GpNDi3jL_!`!vf%oeM;P_!vf(jYpm-PX+~ zY*}Xqz}JV`Ce6$}>3A|nC1=T@p}H%&SCJ&%p|d5L#ZzRfGIZZoTbEp4j7b6x%6>+Dap4gYI46Zh7{~>(;)p6 z_|d;~tUVU>ajENVEu z;BKyf6)n$SZV@RXBKYPuvbP~#gC{=&iiI$W$v_k=;*5Vv19_5vnovq{ec~>SY!ToR$9BeeR%&4XItoAYl2lun5IYfHtQIW zdj9Ea;{Sw}V$N#uq`S2SBY=nK4+)cH?NJ^S3@*OtM*I3 zdbL@Jv$0KZ1W7Nf>vpL(G9wPGR_1GX(n}1jAc&g$20Ln;#&X+lPlvDY%8A+mmV+ zS>8R91Lo&3;Y6XVPo`Pjd$kt6^pHM7hsRlO3v0)r(IB0#fm9b04G|Jz8i#FuVklIGdoowEp+{|(i>7Z^B;-zLS{k=i4^Q{dwo0tC)pn~6ydUBg#vH_* zjE#29H7G;m=SOA5^7V_*I}Z$?mV$%#0Ge6?aS)_t{LV%i?c_)Y=>>J;>7`@HSQ5od zWK&HxUl%P0SbK=VN5KqDN`^z}qO75&3zVfLGDZ7DLer63Fzb`lL%s2h({^qo+2p~N z2jm{5LdFNyVaqErN#2;}T4I9;(4H`OPOinUXV||zl&$IZtzZxf3O-A7r-SL&EzmOF z7c<)H^8#H+X6!yegNF&r{1GmiK35U&eBbxrZ>96idt*@L4n(#`#?mW|7aQ>1(R;4?{POnz+(@4HR1D zQt$V_s9hdmdE@GZ&C~cioQ)Bc@EaPdKCMprLwflqlJXxlrS2_@Z3hF1lmV9Pq`!hET zzfzf(o}^B&!cvAXt$^PWv@I49aK!?!c(O1gT;jAdniW^mL$WRqTR27wM$z@;s1d6Z z?9RGjDP1JVl$ZNxxhk)BWaZ+hQZL3l$1eA(IV7gsLb4g&LYI>p*T*Hp_eS4uM7H_ zl9lOMrXwWes!7DN4fkQp=FE{V#Aw=>q@=JXNwAOU0HWvaAqZ1F)XXp%kDQ+F0Noqw zm1!a2c0PQ)`2p==x@nQU;WOfrW z=|Iz_*~wTVT~f`?CYY397j22i8veWms32Pt9Odh<^kEUMw|*W?Ag1JF5LD>ltnBNrX=@(ydsmO#pPRyUKU5)aQZ>!^n~z{o){1x`rXi1`yB$ z%GL3v3|KvN(#Nl!7rO#Tvnqxy#+p99;WSnD3dJlgv$>~%uL^tIbE9=oa(oywM`wa> zES8QOIm?`-UgMti(A~L`;*a~G19G}cc43@zK7v(*<3a^%Uc_;3p%9K>HfdGoI@2zW z@bh}#z&IT&JWv?@tmkz^CjWq@(&->jN#jA5r%+bzt^U4Ld@4$<&c>s~teo{WOL9DYVmZ&TL$fG(3BHC*K2($ez5S z&D%oN?#6k8`LWV!YI-Q$e<!x390&U!4BB`qrBIkJ-{W!Uwc|c!Gp|!u}`k1>{}g z!IKD){AWBTQXPN1r*8-R1-_!|`=_6Nsp}Yi`lpdQ9{uNkvA;~74`U9H4nL<{ z97DyrL209DvFRxQny^Er89et+nB|^)M4P3MU!{sM4bf2|81W%+B&Fo)MWS)!kd{*h zrBygr;ZGmmFCcTkRJjlrJE0p)?a0@rT!AQv^aG?S)I!{^Y#KJE!$pA3Nvf{50VrQI zWU&_ohU{3hMv5Vn_73P4e0H#F1TBv7oQnUQ8zlB>HgG#Z08W@@?mUQfraGzoq^0P| zF4Kx@A)(}@!aznnK12`j77j@xmGIVP3==omdbvbz6dO?s_>T9y39oRAjWKLCay|M~ zA?MTFY`MDYXei)Xw^espA6?6t+7_JshP4NosPeZAWrt$dT0}FQYy>y)Ft=5Uo$X=i z`fkX*ZMRmzeDG-Tu{dK|EoV!~zU%HJrX=;S>o8|9fG&?qcS>A*U1vw%s4$Ew7OLAF zQ7>Zf|K<6nO@$eUQqa^C#3m>@Dl2Ek7)C(!Parl&Y-$L?0yb%TegWW`(kxo>SQU?+~FM1QYb~X zI;RaW#vfT@rdH~%F_6145xdbX>jMmPFnd?*02<60sOMOg$(630p^<$~$b&I21Y7s! z7G&Y!1{zij5}SqBX8I! zFVg{1H?7Fi6A)XocKe{RRXnX9WQ6lhqD+mROp0n98j576lP3+)?kN)Y+~6bijGFm$ z3bSt!mVt+m$v^HB*YU%#q5-)`EJU!@oIIQ|yS+&gI1%=pu0XEJbv-!8A^|AyoFh+k z2XXin_mg>k!-+sQ+PsCAegjrcTw8y9!m%8S|$z@(-8ewmMsPLAG+xw7Lx%lg&0QA&<_W-!D9HiN0&7#Xs(u+Po(bR; zS(jH&8rC|=t!_s(Q|^(t{dO|p37Y3cbS{-~SCrJzzTY~ZaD}Gu_~~2Zyw^Q=epgDe zp7n}R4YnrwQj=uEvs}BuxR~{WaIqj3Wg3QQq-AAh9VE&j=DsqvczQ(a6Y_ zH3Wmt8X;#SjGR?Ec{^|cE9zaq0N};@1r_sSs5Ljxr1x3^&&qB0^`AV2q6z1iq>yEk z={KnI$^F$4!;7iRnLf-Uva5#&4RsNs4(2PvI$ao zk7*#r016};4H)Z=%vCgwwKWzy=_0C;yWS+g9-L=Vi887fn_V?A5{0j%g?nP6B{HeY zokG>6Mt4?{Njr};Rrny&rAuJh1w@j9ndE za~0Z4#g@8g0U^F*o)M``K_o1S9!zc$AWlK{8Y2|z;j(SiYKQdSL*)Z4i^$xdsI8{- zOSdZfLM{ZvHA)h0Wt-<<1knExOq#uMihlk7Zs{;#5e?rq;eyxKCrhyY zv=3`w>Pe`7SMT26+{QY3AyV4gQS09olb7rG>wa9_vzq$@TeLE%PTNxayaj8Sd%k>- z*o)OK|MIR4@SWUTB?#i8r`kVg?CQ@khs@XAoV~+R;MbR$x5r}W;r`8Nc!TjJKvP-g zxtyPY;na+q7s^|wj>$CUj~B!qf@n6N3*FO1-{pJAliUi4`BZs!VOvp~#K3KLlfK=E zjD^jC7J}KeA7%?PYV%_w&alc26lsQ?%m>UN{sQ}L6b5})zO)ZSEH!jb2 zY=$P51gdhe!qIpfDz(*aX~SJdnw)T8A)?nl3dbprCTvEoQ%lbJZfKwB6jBO~l_uv`c%7&mf@+d-YJ+2kLi- zzia8n@>W~YW7|_OiHb8)f^6i62*Zz|r_qcs;>~=W#xlSva#EL)1YQ0RB4s=)9#z_& zDyx)xd!LyB{JIFaW!kBu_#l$J zfpU*$h+kLAIVn(a6@;@4ZM~MuS3za6bPS2hmk=n$-U%$cq<`+i3X0vDc;>J%M=Bu; zqw159Byy-`D0>)olxf}JO?kV4@a{VHw}%KL#MRF&`$e6c$C@@yMsbs2m^m_Gu#zjo zZ)SfA?ko%8EHyO7hKVu8+tepP+FJWf2446EB4oCGn<(AZes7*YJ2u!Q8f@u{);ym~ zgBvgNzd@&P%KDMq&=cs}A~-pW91Tul-|DbzZO}~UF?xS_yIu^1>&Xx!aWfNYyFqTs zO^}%VT|HqSm{cwfrfvSBEEig8w-Y(wrgQ8msc|~6%OIbHG3{@+CWutF3CgPr-yR4|CG z5Xga%$pMib7nD!yV~{NAx6t5J1`{vHBe<@yW)_UD2y_U;m6;qH5&e8YWA{Mq5Nv|C z%BsooWdj&H1PazLB=mBDcjs{qKQMpy|0G#Xo-c&@?_R^99V>nV`(VVwzCNLRcQ<@N zYBXE-wl*=Mb}@gcXy66o26&UZ;p(E&ss;>Xw#dqx)`1cg&0taCNkuc1nJ5;hE|u|~ z*r*2YA_C*54NqOfvNVe!oG4#lRCN`3WA2snFQfF?A&{wvl)=nDM<#{+h2lkiS?jp z4S0WySeZy|0R=p>@XRuvPEqzbJ-x_|)RnPPKGyw4$Aq|O%WX0QlTwleS2-#LL54jN6SqBeNysgAD*(>x$6VG?%^bQ?2!xby(_(sxL39>V zN0s`~yO0kSuGfV+-?KKA8n(ce7y5GFFt0rB@-2zEx3T zb;|saRog8cLYdN7d}^YYgE;PUAs#)|3GA(;vTZ>G&ghIedJH$|PGNj;I~fFV<#oHp zy!cHU@VC`&(G13=8UQG498`9vD#|r1n1s8f9f^K(XGv|QOUxEJ(>HUbg(Nmg+3gk< zO)Z)l{VpE|pGCM^Na3*WLM&r!99Ide<8nQKBfL#e&y@DaCS{9eU;{J9$RQ7?3y|I< zCTOy6Mo}gc3%Ck1YpHM*j)(v1KG1O%Q^+S5KU(`L9eH%43^V7~a-lE5%Xv~x-dg-a zvB`PrCreAOJMwmve>}ZnMfBE6a?Y^y+dYupx!{>dZ5pJtjN2LI=A4Qo6Wdc1_PCnX z*dWj+{P0k*C7M{j@lHKH&1cWQc@*{DjwBisR*pX-m@9G4ql3S2e)P z;YUZJ74;NBREqox0CF^UkwFAQ*Eu^S7)~{JFXEN`KKIw z6rk(^b$C7W%d*PRLyg>>g?XNNB4==|M$0l4(kB{fC2V!`{=ZT z`SZ`e00$oIwdU)q!>Si|(`5Jw{UdiD3=vQ@4thh)a;`d5yy#+U48@LNs9`;xUMubh zHh$G+VFm2VrP_qPV5XtlmTA&%JBF@3c|i+__H=eLI?qtbLuES~Xkbm#MIsQg%Fw)D zNNsdnX7-)*vecVwasm_7?OLi^QWiGD23rJ+fwE{yUa(EC-dv4Z{5H5`R;--$(#eQnNi!894|m-i{E9b04pfHbr8euW*RUGFv2nr#o1@Qyy_{`&bJHfgaob790HNZHQ$mD9w5?NuTVUh zDst}s`Pw0kr?W@TU%dU{*Mt3&(9vd4XadWI?D{(}spvRuoMW#vV3oD&r-F??wSe@a zppZ_?QiCSMTn2ttwNe#!QloAMr3ExPnaYJDYlWPU!eKm0*fEY6FQC7WW9EG;c~-ub z^1>^@4ab4hJ^2rw)%?i4Dt=!k{}cB6Um3Q)W4u*-=mb*b4!ML$Z3Pk(^QJ&yN+U?N zGLZj9CKFv1wf(X>wa3;PvZ`Jsa1(hpi#w63c5%-aYt;=K8_oAK0~KC#qp}Rt^(c7U zRL*4+poUxWr$D∋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 literal 0 HcmV?d00001 diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 5e115fab539f66..cfacaab5974e79 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -24,24 +24,53 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); self::$other_font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); self::$font_face_id1 = $factory->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, + wp_slash( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + 'post_status' => 'publish', + 'post_title' => 'Open Sans', + 'post_name' => 'open-sans', + 'post_content' => wp_json_encode( + array( + 'font_face_settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), + ), + ) + ), + ) ) ); self::$font_face_id2 = $factory->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, + wp_slash( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + 'post_status' => 'publish', + 'post_title' => 'Open Sans', + 'post_name' => 'open-sans', + 'post_content' => wp_json_encode( + array( + 'font_face_settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '900', + 'fontStyle' => 'normal', + 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ), + ), + ) + ), + ) ) ); - self::$admin_id = $factory->user->create( + self::$admin_id = $factory->user->create( array( 'role' => 'administrator', ) ); - self::$editor_id = $factory->user->create( array( 'role' => 'editor', @@ -49,29 +78,34 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ); } + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_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[\d]+)/font-faces', + '/wp/v2/font-families/(?P[\d]+)/font-faces', $routes, 'Font faces collection for the given font family does not exist' ); $this->assertCount( 2, - $routes['/wp/v2/font-families/(?P[\d]+)/font-faces'], + $routes['/wp/v2/font-families/(?P[\d]+)/font-faces'], 'Font faces collection for the given font family does not have exactly two elements' ); $this->assertArrayHasKey( - '/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)', + '/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)', $routes, 'Single font face route for the given font family does not exist' ); $this->assertCount( 2, - $routes['/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)'], + $routes['/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)'], 'Font faces collection for the given font family does not have exactly two elements' ); } @@ -94,8 +128,10 @@ public function test_get_items() { $this->assertSame( 200, $response->get_status() ); $this->assertCount( 2, $data ); - $this->assertSame( self::$font_face_id1, $data[0]['id'] ); - $this->assertSame( self::$font_face_id2, $data[1]['id'] ); + $this->assertArrayHasKey( '_links', $data[0] ); + $this->check_font_face_data( $data[0], self::$font_face_id1, $data[0]['_links'] ); + $this->assertArrayHasKey( '_links', $data[1] ); + $this->check_font_face_data( $data[1], self::$font_face_id2, $data[1]['_links'] ); } /** @@ -131,12 +167,8 @@ public function test_get_item() { $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $fields = array( - 'id', - 'parent', - ); - $data = $response->get_data(); - $this->assertSameSets( $fields, array_keys( $data ) ); + $data = $response->get_data(); + $this->check_font_face_data( $data, self::$font_face_id1, $response->get_links() ); } /** @@ -184,7 +216,7 @@ public function test_get_item_valid_parent_id() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( self::$font_family_id, $data['parent'], "The returned revision's id should match the parent id." ); + $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); } /** @@ -205,18 +237,234 @@ public function test_get_item_invalid_parent_id() { */ public function test_create_item() { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $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', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => array_keys( $files )[0], + ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $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( + $settings, + array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + ) + ); + + $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @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', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => array_keys( $files ), + ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $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'] ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @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', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ) + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->check_font_face_data( $data, $data['id'], $response->get_links() ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @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', 2 ); + $request->set_param( 'font_face_settings', wp_json_encode( $properties ) ); + $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $fields = array( - 'id', - 'parent', + $this->assertArrayHasKey( 'font_face_settings', $data ); + $this->assertSame( $properties, $data['font_face_settings'] ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @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', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ) + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @dataProvider data_create_item_invalid_theme_json_version + * + * @covers WP_REST_Font_Faces_Controller::create_item + */ + 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 ); + } + + public function data_create_item_invalid_theme_json_version() { + return array( + array( 1 ), + array( 3 ), ); - $data = $response->get_data(); - $this->assertSameSets( $fields, array_keys( $data ) ); + } + + /** + * @dataProvider data_create_item_invalid_settings + * + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + 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', 2 ); + $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } - $this->assertSame( self::$font_family_id, $data['parent'], "The returned revision's id should match the parent id." ); + public function data_create_item_invalid_settings() { + return array( + 'Missing src' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + ), + ), + 'Invalid src' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'src' => '', + ), + ), + 'Missing fontFamily' => array( + 'settings' => array( + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ), + ), + 'Invalid fontDisplay' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontDisplay' => 'invalid', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ), + ), + ); } public function test_update_item() { @@ -288,10 +536,16 @@ public function test_delete_item_invalid_delete_permissions() { } /** - * @doesNotPerformAssertions + * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response */ public function test_prepare_item() { - // Not yet using the prepare_item method for font faces. + 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 ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->check_font_face_data( $data, self::$font_face_id2, $response->get_links() ); } /** @@ -309,4 +563,54 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'parent', $properties ); $this->assertArrayHasKey( 'font_face_settings', $properties ); } + + protected function check_font_face_data( $data, $post_id, $links ) { + $post = get_post( $post_id ); + + $this->assertArrayHasKey( 'id', $data ); + $this->assertSame( $post->ID, $data['id'] ); + + $this->assertArrayHasKey( 'parent', $data ); + $this->assertSame( $post->post_parent, $data['parent'] ); + + $this->assertArrayHasKey( 'theme_json_version', $data ); + $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'] ); + + $this->assertArrayHasKey( 'font_face_settings', $data ); + $this->assertSame( $post->post_content, wp_json_encode( $data['font_face_settings'] ) ); + + $this->assertNotEmpty( $links ); + $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), $links['self'][0]['href'] ); + $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces' ), $links['collection'][0]['href'] ); + $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->post_parent ), $links['parent'][0]['href'] ); + } + + protected function check_file_meta( $font_face_id, $srcs ) { + $file_meta = get_post_meta( $font_face_id, '_wp_font_face_file' ); + + foreach ( $srcs as $src ) { + $file_name = basename( $src ); + $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 = GUTENBERG_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; + } } From 42565a3f88e576655b785dd759a699a74457bbac Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Fri, 12 Jan 2024 13:43:17 -0600 Subject: [PATCH 03/16] Refactor Font Family Controller (#57785) --- ...class-wp-rest-font-families-controller.php | 573 ++++++++------- .../fonts/font-library/font-library.php | 31 +- .../wpRestFontFacesController.php | 80 +-- .../wpRestFontFamiliesController.php | 675 ++++++++++++++++++ .../installFonts.php | 334 --------- .../uninstallFonts.php | 96 --- 6 files changed, 1055 insertions(+), 734 deletions(-) create mode 100644 phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index ede8762c88c6dc..8abc9894206f38 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -1,12 +1,10 @@ rest_base = 'font-families'; - $this->namespace = 'wp/v2'; - $this->post_type = 'wp_font_family'; + $post_type = 'wp_font_family'; + $this->post_type = $post_type; + + $post_type_obj = get_post_type_object( $post_type ); + $this->rest_base = $post_type_obj->rest_base; + $this->namespace = $post_type_obj->rest_namespace; } /** @@ -44,363 +44,420 @@ public function register_routes() { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'permission_callback' => array( $this, 'get_font_families_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_edit_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, - '/' . $this->rest_base, + '/' . $this->rest_base . '/(?P[\d]+)', array( + 'id' => array( + 'description' => __( 'Unique identifier for the font family.', 'gutenberg' ), + 'type' => 'integer', + 'required' => true, + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_font_families_permissions_check' ), + 'args' => array(), + ), array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'install_fonts' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => array( - 'font_family_settings' => array( - 'required' => true, - 'type' => 'string', - 'validate_callback' => array( $this, 'validate_install_font_families' ), - ), - ), + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_create_edit_params(), ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( array( 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'uninstall_fonts' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => $this->uninstall_schema(), + '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' ), + ), + ), ), ) ); } /** - * Returns validation errors in font families data for installation. + * Checks if a given request has access to font families. * * @since 6.5.0 * - * @param array[] $font_families Font families to install. - * @param array $files Files to install. - * @return array $error_messages Array of error messages. + * @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. */ - private function get_validation_errors( $font_family_settings, $files ) { - $error_messages = array(); + public function get_font_families_permissions_check() { + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font faces.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } - if ( ! is_array( $font_family_settings ) ) { - $error_messages[] = __( 'font_family_settings should be a font family definition.', 'gutenberg' ); - return $error_messages; + /** + * Validates font family 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 false|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 ); + $schema = $this->get_item_schema()['properties']['font_family_settings']; + $required = $schema['required']; + + // Allow setting individual properties if we are updating an existing font family. + if ( isset( $request['id'] ) ) { + unset( $schema['required'] ); } - if ( - ! isset( $font_family_settings['slug'] ) || - ! isset( $font_family_settings['name'] ) || - ! isset( $font_family_settings['fontFamily'] ) - ) { - $error_messages[] = __( 'Font family should have slug, name and fontFamily properties defined.', 'gutenberg' ); + // Check that the font face settings match the theme.json schema. + $valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); - return $error_messages; + if ( is_wp_error( $valid_settings ) ) { + $valid_settings->add_data( array( 'status' => 400 ) ); + return $valid_settings; } - if ( isset( $font_family_settings['fontFace'] ) ) { - if ( ! is_array( $font_family_settings['fontFace'] ) ) { - $error_messages[] = __( 'Font family should have fontFace property defined as an array.', 'gutenberg' ); + // 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: Font family setting key. */ + sprintf( __( 'Font family setting "%s" cannot be empty.', 'gutenberg' ), $key ), + array( 'status' => 400 ) + ); } + } - if ( count( $font_family_settings['fontFace'] ) < 1 ) { - $error_messages[] = __( 'Font family should have at least one font face definition.', 'gutenberg' ); - } + return true; + } - if ( ! empty( $font_family_settings['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $font_family_settings['fontFace'] ); $face_index++ ) { - - $font_face = $font_family_settings['fontFace'][ $face_index ]; - if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { - $error_messages[] = sprintf( - // translators: font face index. - __( 'Font family Font face [%1$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), - $face_index - ); - } - - if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { - $error_messages[] = sprintf( - // translators: font face index. - __( 'Font family Font face [%1$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), - $face_index - ); - } - - if ( isset( $font_face['uploadedFile'] ) ) { - if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { - $error_messages[] = sprintf( - // translators: font face index. - __( 'Font family Font face [%1$s] file is not defined in the request files.', 'gutenberg' ), - $face_index - ); - } - } - } - } + /** + * 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. + * @param WP_REST_Request $request Request object. + * @return array Decoded array font family settings. + */ + public function sanitize_font_family_settings( $value ) { + $settings = json_decode( $value, true ); + + if ( isset( $settings['fontFamily'] ) ) { + $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] ); + } + + // Provide default for preview, if not provided. + if ( ! isset( $settings['preview'] ) ) { + $settings['preview'] = ''; } - return $error_messages; + return $settings; } /** - * Validate input for the install endpoint. + * Deletes a single item. * * @since 6.5.0 * - * @param string $param The font families to install. - * @param WP_REST_Request $request The request object. - * @return true|WP_Error True if the parameter is valid, WP_Error otherwise. + * @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 validate_install_font_families( $param, $request ) { - $font_family_settings = json_decode( $param, true ); - $files = $request->get_file_params(); - $error_messages = $this->get_validation_errors( $font_family_settings, $files ); + public function delete_item( $request ) { + $font_family_id = $request->get_param( 'id' ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - if ( empty( $error_messages ) ) { - return true; + // We don't support trashing for revisions. + 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.", 'gutenberg' ), 'force=true' ), + array( 'status' => 501 ) + ); } - return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) ); + $deleted = parent::delete_item( $request ); + + if ( is_wp_error( $deleted ) ) { + return $deleted; + } + + foreach ( $this->get_font_face_ids( $font_family_id ) as $font_face_id ) { + wp_delete_post( $font_face_id, true ); + } } /** - * Gets the schema for the uninstall endpoint. + * Prepares a single item output for response. * * @since 6.5.0 * - * @return array Schema array. + * @param WP_Post $item Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. */ - public function uninstall_schema() { - return array( - 'font_families' => array( - 'type' => 'array', - 'description' => __( 'The font families to install.', 'gutenberg' ), - 'required' => true, - 'minItems' => 1, - 'items' => array( - 'required' => true, - 'type' => 'object', - 'properties' => array( - 'slug' => array( + public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + $data = array(); + + $data['id'] = $item->ID; + $data['theme_json_version'] = 2; + $data['font_faces'] = $this->get_font_face_ids( $item->ID ); + + $settings = json_decode( $item->post_content, true ); + $properties = $this->get_item_schema()['properties']['font_family_settings']['properties']; + + // Only return the properties defined in the schema. + $data['font_family_settings'] = array_intersect_key( $settings, $properties ); + + $response = rest_ensure_response( $data ); + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + + return $response; + } + + /** + * 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', + 'readonly' => true, + ), + 'theme_json_version' => array( + 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ), + 'type' => 'integer', + 'default' => 2, + 'minimum' => 2, + 'maximum' => 2, + ), + 'font_faces' => array( + 'description' => __( 'The IDs of the child font faces in the font family.', 'gutenberg' ), + 'type' => 'array', + '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 declaration in theme.json format.', 'gutenberg' ), + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => 'Name of the font family preset, translatable.', + 'type' => 'string', + ), + 'slug' => array( + 'description' => 'Kebab-case unique identifier for the font family preset.', + 'type' => 'string', + ), + 'fontFamily' => array( + 'description' => 'CSS font-family value.', + 'type' => 'string', + ), + 'preview' => array( + 'description' => 'URL to a preview image of the font family.', 'type' => 'string', - 'description' => __( 'The font family slug.', 'gutenberg' ), - 'required' => true, ), ), + 'required' => array( 'name', 'slug', 'fontFamily' ), + 'additionalProperties' => false, ), ), ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); } /** - * Removes font families from the Font Library and all their assets. + * Retrieves the query params for the font family 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. + * @return array Collection parameters. */ - public function uninstall_fonts( $request ) { - $fonts_to_uninstall = $request->get_param( 'font_families' ); + public function get_collection_params() { + $params = parent::get_collection_params(); - $errors = array(); - $successes = array(); - - if ( empty( $fonts_to_uninstall ) ) { - $errors[] = new WP_Error( - 'no_fonts_to_install', - __( 'No fonts to uninstall', 'gutenberg' ) - ); - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - $response = rest_ensure_response( $data ); - $response->set_status( 400 ); - return $response; - } - - foreach ( $fonts_to_uninstall as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $result = $font->uninstall(); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } - } - $data = array( - 'successes' => $successes, - 'errors' => $errors, + return array( + 'page' => $params['page'], + 'per_page' => $params['per_page'], + 'search' => $params['search'], ); - return rest_ensure_response( $data ); } /** - * Checks whether the user has permissions to update the Font Library. + * Checks if a given request has access to read items. * * @since 6.5.0 * - * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. + * @return true|WP_Error True if the request has read access, otherwise a WP_Error object. */ - public function update_font_library_permissions_check() { - if ( ! current_user_can( 'edit_theme_options' ) ) { + public function get_font_family_permissions_check() { + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( - 'rest_cannot_update_font_library', - __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font families.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) ); } + return true; } /** - * Checks whether the font directory exists or not. + * Get the params used when creating or updating a font family. * * @since 6.5.0 * - * @return bool Whether the font directory exists. + * @return array Font family create/edit arguments. */ - private function has_upload_directory() { - $upload_dir = wp_get_font_dir()['path']; - return is_dir( $upload_dir ); + public function get_create_edit_params() { + $properties = $this->get_item_schema()['properties']; + return array( + 'theme_json_version' => $properties['theme_json_version'], + // 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.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'validate_font_family_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ), + ), + ); } /** - * Checks whether the user has write permissions to the temp and fonts directories. + * Get the child font face post IDs. * * @since 6.5.0 * - * @return true|WP_Error True if the user has write permissions, WP_Error object otherwise. + * @param int $font_family_id Font family post ID. + * @return int[] Array of child font face post IDs. + * . */ - private function has_write_permission() { - // The update endpoints requires write access to the temp and the fonts directories. - $temp_dir = get_temp_dir(); - $upload_dir = wp_get_font_dir()['path']; - if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { - return false; - } - return true; + protected function get_font_face_ids( $font_family_id ) { + $font_face_ids = get_posts( + array( + 'fields' => 'ids', + 'post_parent' => $font_family_id, + 'post_type' => 'wp_font_face', + 'posts_per_page' => 999, + ) + ); + return $font_face_ids; } /** - * Checks whether the request needs write permissions. + * Prepares links for the request. * * @since 6.5.0 * - * @param array[] $font_family_settings Font family definition. - * @return bool Whether the request needs write permissions. + * @param WP_Post $post Post object. + * @return array Links for the given post. */ - private function needs_write_permission( $font_family_settings ) { - if ( isset( $font_family_settings['fontFace'] ) ) { - foreach ( $font_family_settings['fontFace'] as $face ) { - // If the font is being downloaded from a URL or uploaded, it needs write permissions. - if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) { - return true; - } - } + 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 ), + ); + } + + 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( $this->namespace . '/' . $this->rest_base . '/' . $font_family_id . '/font-faces/' . $font_face_id ), + ); } - return false; + return $links; } /** - * Installs new fonts. - * - * Takes a request containing new fonts to install, downloads their assets, and adds them - * to the Font Library. + * Prepares a single font family post for create or update. * * @since 6.5.0 * - * @param WP_REST_Request $request The request object containing the new fonts to install - * in the request parameters. - * @return WP_REST_Response|WP_Error The updated Font Library post content. + * @param WP_REST_Request $request Request object. + * @return stdClass|WP_Error Post object or WP_Error. */ - public function install_fonts( $request ) { - // Get new fonts to install. - $font_family_settings = $request->get_param( 'font_family_settings' ); - - /* - * As this is receiving form data, the font families are encoded as a string. - * The form data is used because local fonts need to use that format to - * attach the files in the request. - */ - $font_family_settings = json_decode( $font_family_settings, true ); - - $successes = array(); - $errors = array(); - $response_status = 200; - - if ( empty( $font_family_settings ) ) { - $errors[] = new WP_Error( - 'no_fonts_to_install', - __( 'No fonts to install', 'gutenberg' ) - ); - $response_status = 400; - } - - if ( $this->needs_write_permission( $font_family_settings ) ) { - $upload_dir = wp_get_font_dir()['path']; - if ( ! $this->has_upload_directory() ) { - if ( ! wp_mkdir_p( $upload_dir ) ) { - $errors[] = new WP_Error( - 'cannot_create_fonts_folder', - sprintf( - /* translators: %s: Directory path. */ - __( 'Error: Unable to create directory %s.', 'gutenberg' ), - esc_html( $upload_dir ) - ) - ); - $response_status = 500; - } + 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' ); + + if ( isset( $request['id'] ) ) { + $existing_post = $this->get_post( $request['id'] ); + if ( is_wp_error( $existing_post ) ) { + return $existing_post; } - if ( $this->has_upload_directory() && ! $this->has_write_permission() ) { - $errors[] = new WP_Error( - 'cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ) - ); - $response_status = 500; - } + $prepared_post->ID = $existing_post->ID; + $existing_settings = json_decode( $existing_post->post_content, true ); + $settings = array_merge( $existing_settings, $settings ); } - if ( ! empty( $errors ) ) { - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - $response = rest_ensure_response( $data ); - $response->set_status( $response_status ); - return $response; - } + $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'] ); + $prepared_post->post_content = wp_json_encode( $settings ); - // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - $font = new WP_Font_Family( $font_family_settings ); - $result = $font->install( $files ); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } - - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - return rest_ensure_response( $data ); + return $prepared_post; } } diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 70bb50e5dfd731..3f41c13a005144 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -22,13 +22,33 @@ function gutenberg_init_font_library_routes() { // @core-merge: This code will go into Core's `create_initial_post_types()`. $args = array( + 'labels' => array( + 'name' => __( 'Font Families', 'gutenberg' ), + 'singular_name' => __( 'Font Family', 'gutenberg' ), + ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'label' => 'Font Family', 'show_in_rest' => true, + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'read_post' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'edit_post' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_post' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => false, 'rest_base' => 'font-families', 'rest_controller_class' => 'WP_REST_Font_Families_Controller', 'autosave_rest_controller_class' => 'WP_REST_Autosave_Font_Families_Controller', + 'query_var' => false, ); register_post_type( 'wp_font_family', $args ); @@ -44,21 +64,20 @@ function gutenberg_init_font_library_routes() { 'hierarchical' => false, 'show_in_rest' => false, 'rest_base' => 'font-faces', - // TODO: Add custom font capability 'capabilities' => array( 'read' => 'edit_theme_options', 'read_post' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', 'create_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', 'edit_post' => 'edit_theme_options', 'edit_posts' => 'edit_theme_options', - 'publish_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', 'edit_published_posts' => 'edit_theme_options', - 'delete_posts' => 'edit_theme_options', 'delete_post' => 'edit_theme_options', - 'delete_published_posts' => 'edit_theme_options', - 'edit_others_posts' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', ), 'map_meta_cap' => false, 'query_var' => false, diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index cfacaab5974e79..3846acba5a4ba6 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -1,6 +1,6 @@ '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 = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); self::$other_font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); - self::$font_face_id1 = $factory->post->create( - wp_slash( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, - 'post_status' => 'publish', - 'post_title' => 'Open Sans', - 'post_name' => 'open-sans', - 'post_content' => wp_json_encode( - array( - 'font_face_settings' => array( - 'fontFamily' => 'Open Sans', - 'fontWeight' => '400', - 'fontStyle' => 'normal', - 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), - ), - ) - ), - ) + 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 = $factory->post->create( - wp_slash( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, - 'post_status' => 'publish', - 'post_title' => 'Open Sans', - 'post_name' => 'open-sans', - 'post_content' => wp_json_encode( - array( - 'font_face_settings' => array( - 'fontFamily' => 'Open Sans', - 'fontWeight' => '900', - 'fontStyle' => 'normal', - 'src' => home_url( '/wp-content/fonts/open-sans-bold.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' ), ) ); @@ -83,6 +67,22 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$editor_id ); } + public static function create_font_face_post( $parent_id, $settings = array() ) { + $settings = array_merge( self::$default_settings, $settings ); + return self::factory()->post->create( + wp_slash( + array( + 'post_type' => 'wp_font_face', + 'post_status' => 'publish', + 'post_title' => $settings['fontFamily'], + 'post_name' => sanitize_title( $settings['fontFamily'] ), + 'post_content' => wp_json_encode( $settings ), + 'post_parent' => $parent_id, + ) + ) + ); + } + /** * @covers WP_REST_Font_Faces_Controller::register_routes */ diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php new file mode 100644 index 00000000000000..9b896cc9c9732d --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -0,0 +1,675 @@ + '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 = WP_REST_Font_Faces_Controller_Test::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 = WP_REST_Font_Faces_Controller_Test::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' ), + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); + } + + public static function create_font_family_post( $settings = array() ) { + $settings = array_merge( self::$default_settings, $settings ); + return 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( $settings ), + ) + ) + ); + } + + /** + * @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', + $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[\d]+)', + $routes, + 'Single font face route for the given font family does not exist' + ); + $this->assertCount( + 3, + $routes['/wp/v2/font-families/(?P[\d]+)'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * @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' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( 2, $data ); + $this->assertArrayHasKey( '_links', $data[0] ); + $this->check_font_family_data( $data[0], self::$font_family_id1, $data[0]['_links'] ); + $this->assertArrayHasKey( '_links', $data[1] ); + $this->check_font_family_data( $data[1], self::$font_family_id2, $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' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + + 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 ); + } + + /** + * @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_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->check_font_family_data( $data, self::$font_family_id1, $response->get_links() ); + } + + /** + * @covers WP_REST_Font_Faces_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() ); + $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'] ); + } + + /** + * @covers WP_REST_Font_Faces_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_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_id1 ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( 'font_family_settings', wp_json_encode( self::$default_settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->check_font_family_data( $data, $data['id'], $response->get_links() ); + + $settings = $data['font_family_settings']; + $this->assertSame( self::$default_settings, $settings ); + $this->assertEmpty( $data['font_faces'] ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @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' ); + $request->set_param( 'font_family_settings', wp_json_encode( self::$default_settings ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertArrayHasKey( 'theme_json_version', $data ); + $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); + + wp_delete_post( $data['id'], true ); + } + + /** + * @dataProvider data_create_item_invalid_theme_json_version + * + * @covers WP_REST_Font_Faces_Controller::create_item + */ + 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 ); + } + + 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_Faces_Controller::create_item + */ + 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', 2 ); + $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() ); + $response_settings = $data['font_family_settings']; + $this->assertArrayHasKey( 'preview', $response_settings ); + $this->assertSame( '', $response_settings['preview'] ); + + wp_delete_post( $data['id'], true ); + } + + public function data_create_item_with_default_preview() { + $default_settings = array( + 'name' => 'Open Sans', + 'slug' => 'open-sans', + '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_create_item_invalid_settings + * + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + 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', 2 ); + $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 ); + } + + public function data_create_item_invalid_settings() { + $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', + ); + return array( + 'Missing name' => array( + 'settings' => array_diff_key( $default_settings, array( 'name' => '' ) ), + ), + 'Empty name' => array( + 'settings' => array_merge( $default_settings, array( 'name' => '' ) ), + ), + 'Missing slug' => array( + 'settings' => array_diff_key( $default_settings, array( 'slug' => '' ) ), + ), + 'Empty slug' => array( + 'settings' => array_merge( $default_settings, array( 'slug' => '' ) ), + ), + 'Missing fontFamily' => array( + 'settings' => array_diff_key( $default_settings, array( 'fontFamily' => '' ) ), + ), + 'Empty fontFamily' => array( + 'settings' => array_merge( $default_settings, array( 'fontFamily' => '' ) ), + ), + ); + } + + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'font_family_settings', wp_json_encode( self::$default_settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_create', $response, 401 ); + + 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 ); + } + + /** + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + + $updated_settings = array( + 'name' => 'Open Sans', + 'slug' => '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(); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); + $request->set_param( + 'font_family_settings', + wp_json_encode( $updated_settings ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->check_font_family_data( $data, $font_family_id, $response->get_links() ); + $this->assertSame( $updated_settings, $data['font_family_settings'] ); + + wp_delete_post( $font_family_id, true ); + } + + /** + * @dataProvider data_update_item_individual_settings + * @covers WP_REST_Font_Families_Controller::update_item + */ + 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() ); + $key = key( $settings ); + $value = current( $settings ); + $this->assertArrayHasKey( $key, $data['font_family_settings'] ); + $this->assertSame( $value, $data['font_family_settings'][ $key ] ); + + wp_delete_post( $font_family_id, true ); + } + + public function data_update_item_individual_settings() { + return array( + array( array( 'name' => 'Opened Sans' ) ), + array( array( 'slug' => '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_update_item_santize_font_family + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_update_item_santize_font_family( $font_family_setting, $expected ) { + 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( array( 'fontFamily' => $font_family_setting ) ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $expected, $data['font_family_settings']['fontFamily'] ); + + wp_delete_post( $font_family_id, true ); + } + + public function data_update_item_santize_font_family() { + return array( + array( 'Libre Barcode 128 Text', "'Libre Barcode 128 Text'" ), + array( 'B612 Mono', "'B612 Mono'" ), + array( 'Open Sans, Noto Sans, sans-serif', "'Open Sans', 'Noto Sans', sans-serif" ), + ); + } + + /** + * @dataProvider data_update_item_invalid_settings + * @covers WP_REST_Font_Faces_Controller::update_item + */ + 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 ); + } + + public function data_update_item_invalid_settings() { + return array( + 'Empty name' => array( + array( 'name' => '' ), + ), + 'Empty slug' => array( + array( 'slug' => '' ), + ), + 'Empty fontFamily' => array( + array( 'fontFamily' => '' ), + ), + ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::update_item + */ + public function test_update_item_invalid_font_family_id() { + 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( self::$default_settings ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::update_item + */ + public function test_update_item_no_permission() { + 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( self::$default_settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + + 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( self::$default_settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + + /** + * @covers WP_REST_Font_Faces_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() ); + $this->assertNull( get_post( $font_family_id ) ); + } + + /** + * @covers WP_REST_Font_Faces_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 ); + + $request->set_param( 'force', 'false' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); + + // Ensure the post still exists. + $post = get_post( $font_family_id ); + $this->assertNotEmpty( $post ); + + wp_delete_post( $font_family_id, true ); + } + + /** + * @covers WP_REST_Font_Faces_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_Faces_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 ); + + 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 ); + + wp_delete_post( $font_family_id, true ); + } + + /** + * @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_id2 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->check_font_family_data( $data, self::$font_family_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' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'theme_json_version', $properties ); + $this->assertArrayHasKey( 'font_faces', $properties ); + $this->assertArrayHasKey( 'font_family_settings', $properties ); + } + + protected function check_font_family_data( $data, $post_id, $links ) { + $post = get_post( $post_id ); + + $this->assertArrayHasKey( 'id', $data ); + $this->assertSame( $post->ID, $data['id'] ); + + $this->assertArrayHasKey( 'theme_json_version', $data ); + $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'] ); + + $font_face_ids = get_posts( + array( + 'fields' => 'ids', + 'post_parent' => $post_id, + 'post_type' => 'wp_font_face', + 'posts_per_page' => 999, + ) + ); + $this->assertArrayHasKey( 'font_faces', $data ); + $this->assertSame( $font_face_ids, $data['font_faces'] ); + + $this->assertArrayHasKey( 'font_family_settings', $data ); + $this->assertSame( $post->post_content, wp_json_encode( $data['font_family_settings'] ) ); + + $this->assertNotEmpty( $links ); + $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->ID ), $links['self'][0]['href'] ); + $this->assertSame( rest_url( 'wp/v2/font-families' ), $links['collection'][0]['href'] ); + + if ( ! $font_face_ids ) { + return; + } + + // Check font_face links, if present. + $this->assertArrayHasKey( 'font_faces', $links ); + foreach ( $links['font_faces'] as $index => $link ) { + $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->ID . '/font-faces/' . $font_face_ids[ $index ] ), $link['href'] ); + + $embeddable = isset( $link['attributes']['embeddable'] ) + ? $link['attributes']['embeddable'] + : $link['embeddable']; + $this->assertTrue( $embeddable ); + } + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php deleted file mode 100644 index 98c1cb6e13fe5c..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php +++ /dev/null @@ -1,334 +0,0 @@ -set_param( 'font_family_settings', $font_family_json ); - $install_request->set_file_params( $files ); - $response = rest_get_server()->dispatch( $install_request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertCount( count( $expected_response['successes'] ), $data['successes'], 'Not all the font families were installed correctly.' ); - - // Checks that the font families were installed correctly. - for ( $family_index = 0; $family_index < count( $data['successes'] ); $family_index++ ) { - $installed_font = $data['successes'][ $family_index ]; - $expected_font = $expected_response['successes'][ $family_index ]; - - if ( isset( $installed_font['fontFace'] ) || isset( $expected_font['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $installed_font['fontFace'] ); $face_index++ ) { - // Checks that the font asset were created correctly. - if ( isset( $installed_font['fontFace'][ $face_index ]['src'] ) ) { - $this->assertStringEndsWith( $expected_font['fontFace'][ $face_index ]['src'], $installed_font['fontFace'][ $face_index ]['src'], 'The src of the fonts were not updated as expected.' ); - } - // Removes the src from the response to compare the rest of the data. - unset( $installed_font['fontFace'][ $face_index ]['src'] ); - unset( $expected_font['fontFace'][ $face_index ]['src'] ); - unset( $installed_font['fontFace'][ $face_index ]['uploadedFile'] ); - } - } - - // Compares if the rest of the data is the same. - $this->assertEquals( $expected_font, $installed_font, 'The endpoint answer is not as expected.' ); - } - } - - /** - * Data provider for test_install_fonts - */ - public function data_install_fonts() { - - $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path1 ); - - $temp_file_path2 = wp_tempnam( 'Monteserrat-' ); - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path2 ); - - return array( - - 'google_fonts_to_download' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - 'files' => array(), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', - ), - ), - ), - ), - 'errors' => array(), - ), - ), - - 'google_fonts_to_use_as_is' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - 'files' => array(), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - ), - 'errors' => array(), - ), - ), - - 'fonts_without_font_faces' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', - ), - 'files' => array(), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', - ), - ), - 'errors' => array(), - ), - ), - - 'fonts_with_local_fonts_assets' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), - ), - ), - 'files' => array( - 'files0' => array( - 'name' => 'piazzola1.ttf', - 'type' => 'font/ttf', - 'tmp_name' => $temp_file_path1, - 'error' => 0, - 'size' => 123, - ), - 'files1' => array( - 'name' => 'montserrat1.ttf', - 'type' => 'font/ttf', - 'tmp_name' => $temp_file_path2, - 'error' => 0, - 'size' => 123, - ), - ), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', - ), - ), - ), - ), - 'errors' => array(), - ), - ), - ); - } - - /** - * Tests failure when fonfaces has improper inputs - * - * @dataProvider data_install_with_improper_inputs - * - * @param array $font_families Font families to install in theme.json format. - * @param array $files Font files to install. - */ - public function test_install_with_improper_inputs( $font_families, $files = array() ) { - $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); - $font_families_json = json_encode( $font_families ); - $install_request->set_param( 'font_families', $font_families_json ); - $install_request->set_file_params( $files ); - - $response = rest_get_server()->dispatch( $install_request ); - $this->assertSame( 400, $response->get_status() ); - } - - /** - * Data provider for test_install_with_improper_inputs - */ - public function data_install_with_improper_inputs() { - $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); - file_put_contents( $temp_file_path1, 'Mocking file content' ); - - return array( - 'not a font families array' => array( - 'font_family_settings' => 'This is not an array', - ), - - 'empty array' => array( - 'font_family_settings' => array(), - ), - - 'without slug' => array( - 'font_family_settings' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - ), - ), - ), - - 'with improper font face property' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => 'This is not an array', - ), - ), - - 'with empty font face property' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array(), - ), - ), - - 'fontface referencing uploaded file without uploaded files' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), - ), - ), - 'files' => array(), - ), - - 'fontface referencing uploaded file without uploaded files' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files666', - ), - ), - ), - 'files' => array( - 'files0' => array( - 'name' => 'piazzola1.ttf', - 'type' => 'font/ttf', - 'tmp_name' => $temp_file_path1, - 'error' => 0, - 'size' => 123, - ), - ), - ), - - 'fontface with incompatible properties (downloadFromUrl and uploadedFile together)' => array( - 'font_family_settings' => array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'uploadedFile' => 'files0', - ), - ), - ), - ), - ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php deleted file mode 100644 index 241f26284fe5d2..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php +++ /dev/null @@ -1,96 +0,0 @@ - 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), - ), - ), - ); - - $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); - $font_families_json = json_encode( $mock_families ); - $install_request->set_param( 'font_families', $font_families_json ); - rest_get_server()->dispatch( $install_request ); - } - - public function test_uninstall() { - $font_families_to_uninstall = array( - array( - 'slug' => 'piazzolla', - ), - array( - 'slug' => 'montserrat', - ), - ); - - $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families' ); - $uninstall_request->set_param( 'font_families', $font_families_to_uninstall ); - $response = rest_get_server()->dispatch( $uninstall_request ); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - } - - - public function test_uninstall_non_existing_fonts() { - $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families' ); - - $non_existing_font_data = array( - array( - 'slug' => 'non-existing-font', - 'name' => 'Non existing font', - ), - array( - 'slug' => 'another-not-installed-font', - 'name' => 'Another not installed font', - ), - ); - - $uninstall_request->set_param( 'font_families', $non_existing_font_data ); - $response = rest_get_server()->dispatch( $uninstall_request ); - $data = $response->get_data(); - $this->assertCount( 2, $data['errors'], 'The response should have 2 errors, one for each font family uninstall failure.' ); - } -} From afacdf2d4e683871956a3d3963738213cd4f82bb Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Mon, 15 Jan 2024 13:50:54 -0600 Subject: [PATCH 04/16] Font Family and Font Face REST API endpoints: better data handling and errors (#57843) --- .../class-wp-rest-font-faces-controller.php | 211 +++++++++---- ...class-wp-rest-font-families-controller.php | 38 ++- .../wpRestFontFacesController.php | 285 ++++++++++++++---- .../wpRestFontFamiliesController.php | 111 +++++-- .../wpRestFontFamiliesController/base.php | 43 --- 5 files changed, 484 insertions(+), 204 deletions(-) delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php index c6b51dc17060b9..4a5d1914095482 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -120,7 +120,7 @@ public function register_routes() { } /** - * Checks if a given request has access to read posts. + * Checks if a given request has access to font faces. * * @since 6.5.0 * @@ -140,41 +140,69 @@ public function get_font_faces_permissions_check() { 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 false|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 ); - $schema = $this->get_item_schema()['properties']['font_face_settings']; + + // 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.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } // Check that the font face settings match the theme.json schema. - $valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); + $schema = $this->get_item_schema()['properties']['font_face_settings']; + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); - // Some properties trigger a multiple "oneOf" types error that we ignore, because they are still valid. - // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric type checking. - if ( is_wp_error( $valid_settings ) && $valid_settings->get_error_code() !== 'rest_one_of_multiple_matches' ) { - $valid_settings->add_data( array( 'status' => 400 ) ); - return $valid_settings; + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; } - $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); - $files = $request->get_file_params(); - - // 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 ) ) { + // 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: A URL. */ - __( 'Every file uploaded must be used as a font face src.', 'gutenberg' ), + /* translators: %s: Font family setting key. */ + sprintf( __( 'font_face_setting[%s] cannot be empty.', 'gutenberg' ), $key ), array( 'status' => 400 ) ); } } - // Check that src strings are non-empty. - foreach ( $srcs as $src ) { - if ( ! $src ) { + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + + // Check that srcs are non-empty strings. + $filtered_src = array_filter( array_filter( $srcs, 'is_string' ) ); + if ( empty( $filtered_src ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Check that each file in the request references a src in the settings. + $files = $request->get_file_params(); + foreach ( array_keys( $files ) as $file ) { + if ( ! in_array( $file, $srcs, true ) ) { return new WP_Error( 'rest_invalid_param', - __( 'Font face src values must be non-empty strings.', 'gutenberg' ), + // translators: %s: File key (e.g. `file-0`) in the request data. + sprintf( __( 'File %1$s must be used in font_face_settings[src].', 'gutenberg' ), $file ), array( 'status' => 400 ) ); } @@ -183,6 +211,26 @@ public function validate_create_font_face_settings( $value, $request ) { 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. + * @param WP_REST_Request $request Request object. + * @return array Decoded 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 ); + + if ( isset( $settings['fontFamily'] ) ) { + $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] ); + } + + return $settings; + } + /** * Retrieves a collection of font faces within the parent font family. * @@ -201,7 +249,7 @@ public function get_items( $request ) { } /** - * Retrieves a single font face for within parent font family. + * Retrieves a single font face within the parent font family. * * @since 6.5.0 * @@ -214,6 +262,7 @@ public function get_item( $request ) { return $post; } + // Check that the font face has a valid parent font family. $font_family = $this->get_font_family_post( $request['font_family_id'] ); if ( is_wp_error( $font_family ) ) { return $font_family; @@ -242,8 +291,8 @@ public function get_item( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_item( $request ) { - // Settings arrive as stringified JSON, since this is a multipart/form-data request. - $settings = json_decode( $request->get_param( 'font_face_settings' ), true ); + // Settings have already been decoded by ::sanitize_font_face_settings(). + $settings = $request->get_param( 'font_face_settings' ); $file_params = $request->get_file_params(); // Move the uploaded font asset from the temp folder to the fonts directory. @@ -264,12 +313,8 @@ public function create_item( $request ) { $file = $file_params[ $src ]; $font_file = $this->handle_font_file_upload( $file ); - if ( isset( $font_file['error'] ) ) { - return new WP_Error( - 'rest_font_upload_unknown_error', - $font_file['error'], - array( 'status' => 500 ) - ); + if ( is_wp_error( $font_file ) ) { + return $font_file; } $processed_srcs[] = $font_file['url']; @@ -336,7 +381,20 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $data['id'] = $item->ID; $data['theme_json_version'] = 2; $data['parent'] = $item->post_parent; - $data['font_face_settings'] = json_decode( $item->post_content, true ); + + $settings = json_decode( $item->post_content, true ); + $properties = $this->get_item_schema()['properties']['font_face_settings']['properties']; + + // Provide required, empty settings if the post_content is not valid JSON. + if ( null === $settings ) { + $settings = array( + 'fontFamily' => '', + 'src' => array(), + ); + } + + // Only return the properties defined in the schema. + $data['font_face_settings'] = array_intersect_key( $settings, $properties ); $response = rest_ensure_response( $data ); $links = $this->prepare_links( $item ); @@ -369,7 +427,7 @@ public function get_item_schema() { 'readonly' => true, ), 'theme_json_version' => array( - 'description' => __( 'Version of the theme.json schema used for the font face typography settings.', 'gutenberg' ), + 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ), 'type' => 'integer', 'default' => 2, 'minimum' => 2, @@ -398,14 +456,9 @@ public function get_item_schema() { 'fontWeight' => array( 'description' => 'List of available font weights, separated by a space.', 'default' => '400', - 'oneOf' => array( - array( - 'type' => 'string', - ), - array( - 'type' => 'integer', - ), - ), + // 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' ), ), 'fontDisplay' => array( 'description' => 'CSS font-display value.', @@ -421,7 +474,8 @@ public function get_item_schema() { ), 'src' => array( 'description' => 'Paths or URLs to the font files.', - 'oneOf' => array( + // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array. + 'anyOf' => array( array( 'type' => 'string', ), @@ -494,30 +548,12 @@ public function get_item_schema() { * @return array Collection parameters. */ public function get_collection_params() { + $params = parent::get_collection_params(); + return array( - 'page' => array( - 'description' => __( 'Current page of the collection.', 'default' ), - 'type' => 'integer', - 'default' => 1, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - 'minimum' => 1, - ), - 'per_page' => array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'default' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ), - 'search' => array( - 'description' => __( 'Limit results to those matching a string.', 'default' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ), + 'page' => $params['page'], + 'per_page' => $params['per_page'], + 'search' => $params['search'], ); } @@ -539,6 +575,7 @@ public function get_create_params() { 'type' => 'string', 'required' => true, 'validate_callback' => array( $this, 'validate_create_font_face_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ), ), ); } @@ -610,10 +647,18 @@ protected function prepare_links( $post ) { return $links; } + /** + * Prepares a single font face post for creation. + * + * @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 and processed by create_item(). + // Settings have already been decoded by ::sanitize_font_face_settings(). $settings = $request->get_param( 'font_face_settings' ); $prepared_post->post_type = $this->post_type; @@ -626,19 +671,30 @@ protected function prepare_item_for_database( $request ) { return $prepared_post; } + /** + * 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 Array containing uploaded file attributes on success, or error on failure. + */ protected function handle_font_file_upload( $file ) { add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_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', + 'action' => 'wp_handle_font_upload', // Not testing a form submission. - 'test_form' => false, + 'test_form' => false, // Seems mime type for files that are not images cannot be tested. // See wp_check_filetype_and_ext(). - 'test_type' => true, + 'test_type' => true, + // Only allow uploading font files for this request. + 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), ); $uploaded_file = wp_handle_upload( $file, $overrides ); @@ -649,6 +705,27 @@ protected function handle_font_file_upload( $file ) { 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. * diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index 8abc9894206f38..0c4b3d8c6c0c77 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -117,7 +117,7 @@ public function get_font_families_permissions_check() { } /** - * Validates font family settings when creating or updating a font family. + * Validates settings when creating or updating a font family. * * @since 6.5.0 * @@ -127,6 +127,16 @@ public function get_font_families_permissions_check() { */ 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', + __( 'font_family_settings parameter must be a valid JSON string.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + $schema = $this->get_item_schema()['properties']['font_family_settings']; $required = $schema['required']; @@ -136,11 +146,11 @@ public function validate_font_family_settings( $value, $request ) { } // Check that the font face settings match the theme.json schema. - $valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); - if ( is_wp_error( $valid_settings ) ) { - $valid_settings->add_data( array( 'status' => 400 ) ); - return $valid_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. @@ -149,7 +159,7 @@ public function validate_font_family_settings( $value, $request ) { return new WP_Error( 'rest_invalid_param', /* translators: %s: Font family setting key. */ - sprintf( __( 'Font family setting "%s" cannot be empty.', 'gutenberg' ), $key ), + sprintf( __( 'font_family_settings[%s] cannot be empty.', 'gutenberg' ), $key ), array( 'status' => 400 ) ); } @@ -183,7 +193,7 @@ public function sanitize_font_family_settings( $value ) { } /** - * Deletes a single item. + * Deletes a single font family. * * @since 6.5.0 * @@ -216,7 +226,7 @@ public function delete_item( $request ) { } /** - * Prepares a single item output for response. + * Prepares a single font family output for response. * * @since 6.5.0 * @@ -234,6 +244,16 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $settings = json_decode( $item->post_content, true ); $properties = $this->get_item_schema()['properties']['font_family_settings']['properties']; + // Provide empty settings if the post_content is not valid JSON. + if ( null === $settings ) { + $settings = array( + 'name' => '', + 'slug' => '', + 'fontFamily' => '', + 'preview' => '', + ); + } + // Only return the properties defined in the schema. $data['font_family_settings'] = array_intersect_key( $settings, $properties ); @@ -333,7 +353,7 @@ public function get_collection_params() { } /** - * Checks if a given request has access to read items. + * Checks if a given request has access to read font families. * * @since 6.5.0 * diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 3846acba5a4ba6..9b5b596e41ac63 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -21,19 +21,19 @@ class WP_REST_Font_Faces_Controller_Test extends WP_Test_REST_Controller_Testcas protected static $font_face_id2; protected static $default_settings = array( - 'fontFamily' => 'Open Sans', + '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 = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); - self::$other_font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); + self::$font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); + self::$other_font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); self::$font_face_id1 = self::create_font_face_post( self::$font_family_id, array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), @@ -43,7 +43,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$font_face_id2 = self::create_font_face_post( self::$font_family_id, array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '900', 'fontStyle' => 'normal', 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ), @@ -165,12 +165,56 @@ 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 ); - $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); - $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $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() ); + $this->assertArrayNotHasKey( 'extra', $data['font_face_settings'] ); + } + + /** + * @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', + ) + ); + + $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() ); + $this->assertSame( $empty_settings, $data['font_face_settings'] ); + + wp_delete_post( $font_face_id, true ); + } + /** * @covers WP_REST_Font_Faces_Controller::get_item */ @@ -216,6 +260,7 @@ public function test_get_item_valid_parent_id() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); } @@ -245,7 +290,7 @@ public function test_create_item() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => array_keys( $files )[0], @@ -257,6 +302,7 @@ public function test_create_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); $this->check_file_meta( $data['id'], array( $data['font_face_settings']['src'] ) ); @@ -265,7 +311,7 @@ public function test_create_item() { $this->assertSame( $settings, array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', ) @@ -289,7 +335,7 @@ public function test_create_item_with_multiple_font_files() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => array_keys( $files ), @@ -301,6 +347,7 @@ public function test_create_item_with_multiple_font_files() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); $this->check_file_meta( $data['id'], $data['font_face_settings']['src'] ); @@ -310,6 +357,41 @@ public function test_create_item_with_multiple_font_files() { wp_delete_post( $data['id'], true ); } + /** + * @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', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( '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 */ @@ -321,7 +403,7 @@ public function test_create_item_with_url_src() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', @@ -332,6 +414,7 @@ public function test_create_item_with_url_src() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); wp_delete_post( $data['id'], true ); @@ -344,7 +427,7 @@ public function test_create_item_with_all_properties() { wp_set_current_user( self::$admin_id ); $properties = array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '300 500', 'fontStyle' => 'oblique 30deg 50deg', 'fontDisplay' => 'swap', @@ -368,6 +451,7 @@ public function test_create_item_with_all_properties() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->assertArrayHasKey( 'font_face_settings', $data ); $this->assertSame( $properties, $data['font_face_settings'] ); @@ -384,7 +468,7 @@ public function test_create_item_default_theme_json_version() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', ) ) @@ -393,6 +477,8 @@ public function test_create_item_default_theme_json_version() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); + $this->assertArrayHasKey( 'theme_json_version', $data ); $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); wp_delete_post( $data['id'], true ); @@ -432,41 +518,118 @@ public function test_create_item_invalid_settings( $settings ) { $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function data_create_item_invalid_settings() { return array( - 'Missing src' => array( - 'settings' => array( - 'fontFamily' => 'Open Sans', - 'fontWeight' => '400', - 'fontStyle' => 'normal', - ), + 'Missing fontFamily' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ), ), - 'Invalid src' => array( - 'settings' => array( - 'fontFamily' => 'Open Sans', - 'src' => '', - ), + 'Empty fontFamily' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ), ), - 'Missing fontFamily' => array( - 'settings' => array( - 'fontWeight' => '400', - 'fontStyle' => 'normal', - 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', - ), + '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( '', '' ) ), ), - 'Invalid fontDisplay' => array( - 'settings' => array( - 'fontFamily' => 'Open Sans', - 'fontDisplay' => 'invalid', - 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', - ), + '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', 2 ); + $request->set_param( 'font_face_settings', 'invalid' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $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 ); + } + + /** + * @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 ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( 'src' => 'invalid' ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $expected_message = 'File ' . array_keys( $files )[0] . ' 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 ); + } + + /** + * @dataProvider data_create_item_santize_font_family + * + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_create_item_santize_font_family( $font_family_setting, $expected ) { + $settings = array_merge( self::$default_settings, array( 'fontFamily' => $font_family_setting ) ); + + 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(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( $expected, $data['font_face_settings']['fontFamily'] ); + } + + public function data_create_item_santize_font_family() { + return array( + array( 'Libre Barcode 128 Text', "'Libre Barcode 128 Text'" ), + array( 'B612 Mono', "'B612 Mono'" ), + array( 'Open Sans, Noto Sans, sans-serif', "'Open Sans', 'Noto Sans', sans-serif" ), + ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + // public function test_create_item_no_permission() {} + 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 ); @@ -478,12 +641,7 @@ public function test_update_item() { */ public function test_delete_item() { wp_set_current_user( self::$admin_id ); - $font_face_id = self::factory()->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_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['force'] = true; $response = rest_get_server()->dispatch( $request ); @@ -497,12 +655,7 @@ public function test_delete_item() { */ public function test_delete_item_no_trash() { wp_set_current_user( self::$admin_id ); - $font_face_id = self::factory()->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_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 ); @@ -521,17 +674,27 @@ public function test_delete_item_no_trash() { /** * @covers WP_REST_Font_Faces_Controller::delete_item */ - public function test_delete_item_invalid_delete_permissions() { - wp_set_current_user( self::$editor_id ); - $font_face_id = self::factory()->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_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 ); + 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 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @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 ); + 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 ); } @@ -542,9 +705,9 @@ 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 ); - $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); - $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $this->check_font_face_data( $data, self::$font_face_id2, $response->get_links() ); } @@ -556,6 +719,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $properties = $data['schema']['properties']; $this->assertCount( 4, $properties ); $this->assertArrayHasKey( 'id', $properties ); @@ -597,6 +761,7 @@ protected function check_file_meta( $font_face_id, $srcs ) { protected function setup_font_file_upload( $formats ) { $files = array(); foreach ( $formats as $format ) { + // @core-merge Use `DIR_TESTDATA` instead of `GUTENBERG_DIR_TESTDATA`. $font_file = GUTENBERG_DIR_TESTDATA . 'fonts/OpenSans-Regular.' . $format; $font_path = wp_tempnam( 'OpenSans-Regular.' . $format ); copy( $font_file, $font_path ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index 9b896cc9c9732d..d00d653c0f7274 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -174,7 +174,7 @@ public function test_get_item() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @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() ) ); @@ -186,6 +186,38 @@ public function test_get_item_removes_extra_settings() { $this->assertSame( 200, $response->get_status() ); $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'] ); + + wp_delete_post( $font_family_id, true ); + } + + /** + * @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', + ) + ); + + $empty_settings = array( + 'name' => '', + 'slug' => '', + '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() ); + $this->assertSame( $empty_settings, $data['font_family_settings'] ); + + wp_delete_post( $font_family_id, true ); } /** @@ -278,7 +310,7 @@ public function data_create_item_invalid_theme_json_version() { /** * @dataProvider data_create_item_with_default_preview * - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Faces_Controller::sanitize_font_family_settings */ public function test_create_item_with_default_preview( $settings ) { wp_set_current_user( self::$admin_id ); @@ -321,41 +353,60 @@ 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', 2 ); - $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function data_create_item_invalid_settings() { - $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', - ); return array( - 'Missing name' => array( - 'settings' => array_diff_key( $default_settings, array( 'name' => '' ) ), + 'Missing name' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'name' => '' ) ), ), - 'Empty name' => array( - 'settings' => array_merge( $default_settings, array( 'name' => '' ) ), + 'Empty name' => array( + 'settings' => array_merge( self::$default_settings, array( 'name' => '' ) ), ), - 'Missing slug' => array( - 'settings' => array_diff_key( $default_settings, array( 'slug' => '' ) ), + 'Wrong name type' => array( + 'settings' => array_merge( self::$default_settings, array( 'name' => 1234 ) ), ), - 'Empty slug' => array( - 'settings' => array_merge( $default_settings, array( 'slug' => '' ) ), + 'Missing slug' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'slug' => '' ) ), ), - 'Missing fontFamily' => array( - 'settings' => array_diff_key( $default_settings, array( 'fontFamily' => '' ) ), + 'Empty slug' => array( + 'settings' => array_merge( self::$default_settings, array( 'slug' => '' ) ), ), - 'Empty fontFamily' => array( - 'settings' => array_merge( $default_settings, array( 'fontFamily' => '' ) ), + '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', 2 ); + $request->set_param( 'font_family_settings', 'invalid' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $expected_message = 'font_family_settings parameter must be a valid JSON string.'; + $actual_message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $actual_message ); + } /** * @covers WP_REST_Font_Faces_Controller::create_item @@ -448,6 +499,7 @@ public function data_update_item_individual_settings() { /** * @dataProvider data_update_item_santize_font_family + * * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_santize_font_family( $font_family_setting, $expected ) { @@ -490,15 +542,24 @@ public function test_update_item_empty_settings( $settings ) { public function data_update_item_invalid_settings() { return array( - 'Empty name' => array( + 'Empty name' => array( array( 'name' => '' ), ), - 'Empty slug' => array( + 'Wrong name type' => array( + array( 'name' => 1234 ), + ), + 'Empty slug' => array( array( 'slug' => '' ), ), - 'Empty fontFamily' => array( + 'Wrong slug type' => array( + array( 'slug' => 1234 ), + ), + 'Empty fontFamily' => array( array( 'fontFamily' => '' ), ), + 'Wrong fontFamily type' => array( + array( 'fontFamily' => 1234 ), + ), ); } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php deleted file mode 100644 index e2d190cd76af1f..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php +++ /dev/null @@ -1,43 +0,0 @@ -factory->user->create( - array( - 'role' => 'administrator', - ) - ); - wp_set_current_user( $admin_id ); - } - - /** - * Tear down each test method. - */ - public function tear_down() { - parent::tear_down(); - - // Clean up the /fonts directory. - foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { - @unlink( $file ); - } - } -} From 7c82cb9ecbd7c9e5d4d598d52d188f29b35a9651 Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Tue, 16 Jan 2024 10:45:01 -0600 Subject: [PATCH 05/16] Font Families REST API endpoint: ensure unique font family slugs (#57861) --- .../class-wp-rest-font-faces-controller.php | 39 ++++-- ...class-wp-rest-font-families-controller.php | 104 ++++++++++---- .../wpRestFontFacesController.php | 2 +- .../wpRestFontFamiliesController.php | 127 ++++++++++++++---- 4 files changed, 202 insertions(+), 70 deletions(-) diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php index 4a5d1914095482..7f4fe653a28548 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -381,20 +381,7 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $data['id'] = $item->ID; $data['theme_json_version'] = 2; $data['parent'] = $item->post_parent; - - $settings = json_decode( $item->post_content, true ); - $properties = $this->get_item_schema()['properties']['font_face_settings']['properties']; - - // Provide required, empty settings if the post_content is not valid JSON. - if ( null === $settings ) { - $settings = array( - 'fontFamily' => '', - 'src' => array(), - ); - } - - // Only return the properties defined in the schema. - $data['font_face_settings'] = array_intersect_key( $settings, $properties ); + $data['font_face_settings'] = $this->get_settings_from_post( $item ); $response = rest_ensure_response( $data ); $links = $this->prepare_links( $item ); @@ -748,4 +735,28 @@ protected function relative_fonts_path( $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( + 'src' => array(), + ); + } + $settings['fontFamily'] = $post->post_title ?? ''; + + // Only return the properties defined in the schema. + return array_intersect_key( $settings, $properties ); + } } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index 0c4b3d8c6c0c77..89bc88020e3330 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -140,9 +140,18 @@ public function validate_font_family_settings( $value, $request ) { $schema = $this->get_item_schema()['properties']['font_family_settings']; $required = $schema['required']; - // Allow setting individual properties if we are updating an existing font family. 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', + __( 'font_family_settings[slug] cannot be updated.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } } // Check that the font face settings match the theme.json schema. @@ -192,6 +201,37 @@ public function sanitize_font_family_settings( $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. + $existing_font_family = get_posts( + array( + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'name' => $settings['slug'], + ) + ); + if ( ! empty( $existing_font_family ) ) { + return new WP_Error( + 'rest_duplicate_font_family', + /* translators: %s: Font family slug. */ + sprintf( __( 'A font family with slug "%s" already exists.', 'gutenberg' ), $settings['slug'] ), + array( 'status' => 400 ) + ); + } + + return parent::create_item( $request ); + } + /** * Deletes a single font family. * @@ -237,25 +277,10 @@ public function delete_item( $request ) { public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class $data = array(); - $data['id'] = $item->ID; - $data['theme_json_version'] = 2; - $data['font_faces'] = $this->get_font_face_ids( $item->ID ); - - $settings = json_decode( $item->post_content, true ); - $properties = $this->get_item_schema()['properties']['font_family_settings']['properties']; - - // Provide empty settings if the post_content is not valid JSON. - if ( null === $settings ) { - $settings = array( - 'name' => '', - 'slug' => '', - 'fontFamily' => '', - 'preview' => '', - ); - } - - // Only return the properties defined in the schema. - $data['font_family_settings'] = array_intersect_key( $settings, $properties ); + $data['id'] = $item->ID; + $data['theme_json_version'] = 2; + $data['font_faces'] = $this->get_font_face_ids( $item->ID ); + $data['font_family_settings'] = $this->get_settings_from_post( $item ); $response = rest_ensure_response( $data ); $links = $this->prepare_links( $item ); @@ -349,6 +374,7 @@ public function get_collection_params() { 'page' => $params['page'], 'per_page' => $params['per_page'], 'search' => $params['search'], + 'slug' => $params['slug'], ); } @@ -458,9 +484,10 @@ protected function prepare_font_face_links( $font_family_id ) { */ protected function prepare_item_for_database( $request ) { $prepared_post = new stdClass(); - // Settings have already been decoded by sanitize_font_family_settings(). + // 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 ) ) { @@ -468,16 +495,41 @@ protected function prepare_item_for_database( $request ) { } $prepared_post->ID = $existing_post->ID; - $existing_settings = json_decode( $existing_post->post_content, true ); + $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'] ); + $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' => $post->post_title ?? '', + 'slug' => $post->post_name ?? '', + 'fontFamily' => $settings_json['fontFamily'] ?? '', + 'preview' => $settings_json['preview'] ?? '', + ); + } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 9b5b596e41ac63..d248394e611dbf 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -200,8 +200,8 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { ); $empty_settings = array( - 'fontFamily' => '', 'src' => array(), + 'fontFamily' => '', ); wp_set_current_user( self::$admin_id ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index d00d653c0f7274..1f7e86ab92fe59 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -88,7 +88,12 @@ public static function create_font_family_post( $settings = array() ) { 'post_status' => 'publish', 'post_title' => $settings['name'], 'post_name' => $settings['slug'], - 'post_content' => wp_json_encode( $settings ), + 'post_content' => wp_json_encode( + array( + 'fontFamily' => $settings['fontFamily'], + 'preview' => $settings['preview'], + ) + ), ) ) ); @@ -145,6 +150,23 @@ public function test_get_items() { $this->check_font_family_data( $data[1], self::$font_family_id2, $data[1]['_links'] ); } + /** + * @covers WP_REST_Font_Faces_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() ); + $this->assertCount( 1, $data ); + $this->assertSame( $font_family->ID, $data[0]['id'] ); + } + /** * @covers WP_REST_Font_Faces_Controller::get_items */ @@ -204,7 +226,8 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { $empty_settings = array( 'name' => '', - 'slug' => '', + // Slug will default to the post id. + 'slug' => (string) $font_family_id, 'fontFamily' => '', 'preview' => '', ); @@ -249,19 +272,20 @@ public function test_get_item_no_permission() { * @covers WP_REST_Font_Faces_Controller::create_item */ public function test_create_item() { - wp_set_current_user( self::$admin_id ); + $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', 2 ); - $request->set_param( 'font_family_settings', wp_json_encode( self::$default_settings ) ); + $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() ); $this->check_font_family_data( $data, $data['id'], $response->get_links() ); - $settings = $data['font_family_settings']; - $this->assertSame( self::$default_settings, $settings ); + $reponse_settings = $data['font_family_settings']; + $this->assertSame( $settings, $reponse_settings ); $this->assertEmpty( $data['font_faces'] ); wp_delete_post( $data['id'], true ); @@ -271,9 +295,10 @@ public function test_create_item() { * @covers WP_REST_Font_Faces_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( self::$default_settings ) ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -331,7 +356,7 @@ public function test_create_item_with_default_preview( $settings ) { public function data_create_item_with_default_preview() { $default_settings = array( 'name' => 'Open Sans', - 'slug' => 'open-sans', + 'slug' => 'open-sans-2', 'fontFamily' => '"Open Sans", sans-serif', ); return array( @@ -404,17 +429,35 @@ public function test_create_item_invalid_settings_json() { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); $expected_message = 'font_family_settings parameter must be a valid JSON string.'; - $actual_message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; - $this->assertSame( $expected_message, $actual_message ); + $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $message ); + } + + /** + * @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', 2 ); + $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 ); + $expected_message = 'A font family with slug "helvetica" already exists.'; + $message = $response->as_error()->get_error_messages()[0]; + $this->assertSame( $expected_message, $message ); } /** * @covers WP_REST_Font_Faces_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( self::$default_settings ) ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_create', $response, 401 ); @@ -441,25 +484,31 @@ public function test_create_item_no_permission() { public function test_update_item() { wp_set_current_user( self::$admin_id ); - $updated_settings = array( + $settings = array( 'name' => 'Open Sans', - 'slug' => '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(); + $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( $updated_settings ) + wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->check_font_family_data( $data, $font_family_id, $response->get_links() ); - $this->assertSame( $updated_settings, $data['font_family_settings'] ); + + $expected_settings = array( + 'name' => $settings['name'], + 'slug' => 'open-sans-2', + 'fontFamily' => $settings['fontFamily'], + 'preview' => $settings['preview'], + ); + $this->assertSame( $expected_settings, $data['font_family_settings'] ); wp_delete_post( $font_family_id, true ); } @@ -489,7 +538,6 @@ public function test_update_item_individual_settings( $settings ) { public function data_update_item_individual_settings() { return array( array( array( 'name' => 'Opened Sans' ) ), - array( array( 'slug' => '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. @@ -527,6 +575,7 @@ public function data_update_item_santize_font_family() { /** * @dataProvider data_update_item_invalid_settings + * * @covers WP_REST_Font_Faces_Controller::update_item */ public function test_update_item_empty_settings( $settings ) { @@ -548,12 +597,6 @@ public function data_update_item_invalid_settings() { 'Wrong name type' => array( array( 'name' => 1234 ), ), - 'Empty slug' => array( - array( 'slug' => '' ), - ), - 'Wrong slug type' => array( - array( 'slug' => 1234 ), - ), 'Empty fontFamily' => array( array( 'fontFamily' => '' ), ), @@ -566,14 +609,31 @@ public function data_update_item_invalid_settings() { /** * @covers WP_REST_Font_Faces_Controller::update_item */ - public function test_update_item_invalid_font_family_id() { + 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/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); $request->set_param( 'font_family_settings', - wp_json_encode( self::$default_settings ) + wp_json_encode( array( 'slug' => 'new-slug' ) ) ); $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $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 ); + } + + /** + * @covers WP_REST_Font_Faces_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 ); } @@ -581,15 +641,17 @@ public function test_update_item_invalid_font_family_id() { * @covers WP_REST_Font_Faces_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( self::$default_settings ) ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); 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( self::$default_settings ) ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); } @@ -712,7 +774,14 @@ protected function check_font_family_data( $data, $post_id, $links ) { $this->assertSame( $font_face_ids, $data['font_faces'] ); $this->assertArrayHasKey( 'font_family_settings', $data ); - $this->assertSame( $post->post_content, wp_json_encode( $data['font_family_settings'] ) ); + $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 ); $this->assertNotEmpty( $links ); $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->ID ), $links['self'][0]['href'] ); From f84cc6d1a0cceeaa6e20929e7060f6e748753eb4 Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Tue, 16 Jan 2024 12:37:20 -0600 Subject: [PATCH 06/16] Font Library: delete child font faces and font assets when deleting parent (#57867) Co-authored-by: Sarah Norris <1645628+mikachan@users.noreply.github.com> --- .../fonts/font-library/font-library.php | 57 +++++++++++++ .../fonts/font-library/fontLibraryHooks.php | 85 +++++++++++++++++++ .../wpRestFontFamiliesController.php | 9 +- 3 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 phpunit/tests/fonts/font-library/fontLibraryHooks.php diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 3f41c13a005144..eb76cbab191280 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -187,3 +187,60 @@ function wp_get_font_dir( $defaults = array() ) { return apply_filters( 'font_dir', $defaults ); } } + +// @core-merge: Filters should go in `src/wp-includes/default-filters.php`, +// functions in a general file for font library. +if ( ! function_exists( '_wp_delete_font_family' ) ) { + /** + * Deletes child font faces when a font family is deleted. + * + * @access private + * @since 6.5.0 + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @return void + */ + function _wp_delete_font_family( $post_id, $post ) { + if ( 'wp_font_family' !== $post->post_type ) { + return; + } + + $font_faces = get_children( + array( + 'post_parent' => $post_id, + 'post_type' => 'wp_font_face', + ) + ); + + foreach ( $font_faces as $font_face ) { + wp_delete_post( $font_face->ID, true ); + } + } + add_action( 'deleted_post', '_wp_delete_font_family', 10, 2 ); +} + +if ( ! function_exists( '_wp_delete_font_face' ) ) { + /** + * Deletes associated font files when a font face is deleted. + * + * @access private + * @since 6.5.0 + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @return void + */ + function _wp_delete_font_face( $post_id, $post ) { + if ( 'wp_font_face' !== $post->post_type ) { + return; + } + + $font_files = get_post_meta( $post_id, '_wp_font_face_files', false ); + + foreach ( $font_files as $font_file ) { + wp_delete_file( wp_get_font_dir()['path'] . '/' . $font_file ); + } + } + add_action( 'before_delete_post', '_wp_delete_font_face', 10, 2 ); +} diff --git a/phpunit/tests/fonts/font-library/fontLibraryHooks.php b/phpunit/tests/fonts/font-library/fontLibraryHooks.php new file mode 100644 index 00000000000000..546b08b24ae258 --- /dev/null +++ b/phpunit/tests/fonts/font-library/fontLibraryHooks.php @@ -0,0 +1,85 @@ +post->create( + array( + 'post_type' => 'wp_font_family', + ) + ); + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $font_family_id, + ) + ); + $other_font_family_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_family', + ) + ); + $other_font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $other_font_family_id, + ) + ); + + wp_delete_post( $font_family_id, true ); + + $this->assertNull( get_post( $font_face_id ) ); + $this->assertNotNull( get_post( $other_font_face_id ) ); + } + + public function test_deleting_font_faces_deletes_associated_font_files() { + list( $font_face_id, $font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.woff2' ); + list( , $other_font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.ttf' ); + + wp_delete_post( $font_face_id, true ); + + $this->assertFalse( file_exists( $font_path ) ); + $this->assertTrue( file_exists( $other_font_path ) ); + } + + protected function create_font_face_with_file( $filename ) { + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + ) + ); + + $font_file = $this->upload_font_file( $filename ); + + // Make sure the font file uploaded successfully. + $this->assertFalse( $font_file['error'] ); + + $font_path = $font_file['file']; + $font_filename = basename( $font_path ); + add_post_meta( $font_face_id, '_wp_font_face_files', $font_filename ); + + return array( $font_face_id, $font_path ); + } + + protected function upload_font_file( $font_filename ) { + // @core-merge Use `DIR_TESTDATA` instead of `GUTENBERG_DIR_TESTDATA`. + $font_file_path = GUTENBERG_DIR_TESTDATA . 'fonts/' . $font_filename; + + add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_dir', 'wp_get_font_dir' ); + $font_file = wp_upload_bits( + $font_filename, + null, + file_get_contents( $font_file_path ) + ); + remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + + return $font_file; + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index 1f7e86ab92fe59..b11e0de3d2cc25 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -762,12 +762,11 @@ protected function check_font_family_data( $data, $post_id, $links ) { $this->assertArrayHasKey( 'theme_json_version', $data ); $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'] ); - $font_face_ids = get_posts( + $font_face_ids = get_children( array( - 'fields' => 'ids', - 'post_parent' => $post_id, - 'post_type' => 'wp_font_face', - 'posts_per_page' => 999, + 'fields' => 'ids', + 'post_parent' => $post_id, + 'post_type' => 'wp_font_face', ) ); $this->assertArrayHasKey( 'font_faces', $data ); From 492a3eef6783ca80d9c6bb4a35ee6c674c7d4ae4 Mon Sep 17 00:00:00 2001 From: Jeff Ong Date: Wed, 17 Jan 2024 10:27:28 -0500 Subject: [PATCH 07/16] Font Library: refactor client side install functions to work with revised API (#57844) * Add batchInstallFontFaces function and related plumbing. * Fix resolver name. * Add embedding and rebuild theme.json settings for fontFamily. * Handle responses directly, add to collection before activating. Remove unused test. * Remove getIntersectingFontFaces. * Check for existing font family before installing. * Reference src, not uploadedFile key. Co-authored-by: Matias Benedetto * Check for existing font family using GET /font-families?slug=. * Filter already installed font faces (determined by matching fontWeight AND fontStyle) --------- Co-authored-by: Matias Benedetto Co-authored-by: Jason Crist --- .../font-library-modal/context.js | 125 +++++++++++++++--- .../font-library-modal/resolvers.js | 19 ++- .../font-library-modal/utils/index.js | 94 +++++++++---- .../test/makeFormDataFromFontFamily.spec.js | 58 -------- 4 files changed, 193 insertions(+), 103 deletions(-) delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index e0749845788d60..c6e37d45c689ec 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -14,7 +14,8 @@ import { * Internal dependencies */ import { - fetchInstallFont, + fetchGetFontFamilyBySlug, + fetchInstallFontFamily, fetchUninstallFonts, fetchFontCollections, fetchFontCollection, @@ -26,10 +27,11 @@ import { mergeFontFamilies, loadFontFaceInBrowser, getDisplaySrcFromFontFace, - makeFormDataFromFontFamily, + makeFontFacesFormData, + makeFontFamilyFormData, + batchInstallFontFaces, } from './utils'; import { toggleFont } from './utils/toggleFont'; -import getIntersectingFontFaces from './utils/get-intersecting-font-faces'; export const FontLibraryContext = createContext( {} ); @@ -60,12 +62,19 @@ function FontLibraryProvider( { children } ) { records: libraryPosts = [], isResolving: isResolvingLibrary, hasResolved: hasResolvedLibrary, - } = useEntityRecords( 'postType', 'wp_font_family', { refreshKey } ); + } = useEntityRecords( 'postType', 'wp_font_family', { + refreshKey, + _embed: true, + } ); const libraryFonts = - ( libraryPosts || [] ).map( ( post ) => - JSON.parse( post.content.raw ) - ) || []; + ( libraryPosts || [] ).map( ( post ) => { + post.font_family_settings.fontFace = + post?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || []; + return post.font_family_settings; + } ) || []; // Global Styles (settings) font families const [ fontFamilies, setFontFamilies ] = useGlobalSetting( @@ -195,32 +204,108 @@ function FontLibraryProvider( { children } ) { async function installFont( font ) { setIsInstalling( true ); try { - // Prepare formData to install. - const formData = makeFormDataFromFontFamily( font ); + // Get the ID of the font family post, if it is already installed. + let installedFontFamily = await fetchGetFontFamilyBySlug( + font.slug + ) + .then( ( response ) => { + if ( ! response || response.length === 0 ) { + return null; + } + const fontFamilyPost = response[ 0 ]; + return { + id: fontFamilyPost.id, + ...fontFamilyPost.font_family_settings, + fontFace: + fontFamilyPost?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || [], + }; + } ) + .catch( ( e ) => { + // eslint-disable-next-line no-console + console.error( e ); + return null; + } ); + + // Otherwise, install it. + if ( ! installedFontFamily ) { + const fontFamilyFormData = makeFontFamilyFormData( font ); + // Prepare font family form data to install. + installedFontFamily = await fetchInstallFontFamily( + fontFamilyFormData + ) + .then( ( response ) => { + return { + id: response.id, + ...response.font_face_settings, + fontFace: [], + }; + } ) + .catch( ( e ) => { + throw Error( e.message ); + } ); + } + + // Filter Font Faces that have already been installed + // We determine that by comparing the fontWeight and fontStyle + font.fontFace = font.fontFace.filter( ( fontFaceToInstall ) => { + return ( + -1 === + installedFontFamily.fontFace.findIndex( + ( installedFontFace ) => { + return ( + installedFontFace.fontWeight === + fontFaceToInstall.fontWeight && + installedFontFace.fontStyle === + fontFaceToInstall.fontStyle + ); + } + ) + ); + } ); + + if ( font.fontFace.length === 0 ) { + // Looks like we're only trying to install fonts that are already installed. + // Let's not do that. + // TODO: Exit with an error message? + return { + errors: [ 'All font faces are already installed' ], + }; + } + + // Prepare font faces form data to install. + const fontFacesFormData = makeFontFacesFormData( font ); + // Install the fonts (upload the font files to the server and create the post in the database). - const response = await fetchInstallFont( formData ); - const fontsInstalled = response?.successes || []; - // Get intersecting font faces between the fonts we tried to installed and the fonts that were installed - // (to avoid activating a non installed font). - const fontToBeActivated = getIntersectingFontFaces( - fontsInstalled, - [ font ] + const response = await batchInstallFontFaces( + installedFontFamily.id, + fontFacesFormData ); - // Activate the font families (add the font families to the global styles). - activateCustomFontFamilies( fontToBeActivated ); + + const fontFacesInstalled = response?.successes || []; + + // Rebuild fontFace settings + font.fontFace = + fontFacesInstalled.map( ( face ) => { + return face.font_face_settings; + } ) || []; + + // Activate the font family (add the font family to the global styles). + activateCustomFontFamilies( [ font ] ); // Save the global styles to the database. saveSpecifiedEntityEdits( 'root', 'globalStyles', globalStylesId, [ 'settings.typography.fontFamilies', ] ); refreshLibrary(); - setIsInstalling( false ); return response; } catch ( error ) { - setIsInstalling( false ); return { errors: [ error ], }; + } finally { + setIsInstalling( false ); } } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index 2e7f413a6fa45b..08e37dc7ee95fb 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -7,7 +7,7 @@ */ import apiFetch from '@wordpress/api-fetch'; -export async function fetchInstallFont( data ) { +export async function fetchInstallFontFamily( data ) { const config = { path: '/wp/v2/font-families', method: 'POST', @@ -16,6 +16,23 @@ export async function fetchInstallFont( data ) { return apiFetch( config ); } +export async function fetchInstallFontFace( fontFamilyId, data ) { + const config = { + path: `/wp/v2/font-families/${ fontFamilyId }/font-faces`, + method: 'POST', + body: data, + }; + return apiFetch( config ); +} + +export async function fetchGetFontFamilyBySlug( slug ) { + const config = { + path: `/wp/v2/font-families?slug=${ slug }&_embed=true`, + method: 'GET', + }; + return apiFetch( config ); +} + export async function fetchUninstallFonts( fonts ) { const data = { font_families: fonts, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 0aa0f7edb4aec9..98b6375740e5b4 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -8,6 +8,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; */ import { FONT_WEIGHTS, FONT_STYLES } from './constants'; import { unlock } from '../../../../lock-unlock'; +import { fetchInstallFontFace } from '../resolvers'; /** * Browser dependencies @@ -135,39 +136,84 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } -export function makeFormDataFromFontFamily( fontFamily ) { +export function makeFontFamilyFormData( fontFamily ) { const formData = new FormData(); const { kebabCase } = unlock( componentsPrivateApis ); - const newFontFamily = { - ...fontFamily, + const { fontFace, category, ...familyWithValidParameters } = fontFamily; + const fontFamilySettings = { + ...familyWithValidParameters, slug: kebabCase( fontFamily.slug ), }; - if ( newFontFamily?.fontFace ) { - const newFontFaces = newFontFamily.fontFace.map( - ( face, faceIndex ) => { - if ( face.file ) { - // Slugified file name because the it might contain spaces or characters treated differently on the server. - const fileId = `file-${ faceIndex }`; - // Add the files to the formData - formData.append( fileId, face.file, face.file.name ); - // remove the file object from the face object the file is referenced by the uploadedFile key - const { file, ...faceWithoutFileProperty } = face; - const newFace = { - ...faceWithoutFileProperty, - uploadedFile: fileId, - }; - return newFace; - } - return face; + formData.append( + 'font_family_settings', + JSON.stringify( fontFamilySettings ) + ); + return formData; +} + +export function makeFontFacesFormData( font ) { + if ( font?.fontFace ) { + const fontFacesFormData = font.fontFace.map( ( face, faceIndex ) => { + const formData = new FormData(); + if ( face.file ) { + // Slugified file name because the it might contain spaces or characters treated differently on the server. + const fileId = `file-${ faceIndex }`; + // Add the files to the formData + formData.append( fileId, face.file, face.file.name ); + // remove the file object from the face object the file is referenced in src + const { file, ...faceWithoutFileProperty } = face; + const fontFaceSettings = { + ...faceWithoutFileProperty, + src: fileId, + }; + formData.append( + 'font_face_settings', + JSON.stringify( fontFaceSettings ) + ); + } else { + formData.append( 'font_face_settings', JSON.stringify( face ) ); } - ); - newFontFamily.fontFace = newFontFaces; + return formData; + } ); + + return fontFacesFormData; } +} - formData.append( 'font_family_settings', JSON.stringify( newFontFamily ) ); - return formData; +export async function batchInstallFontFaces( fontFamilyId, fontFacesData ) { + const promises = fontFacesData.map( ( faceData ) => + fetchInstallFontFace( fontFamilyId, faceData ) + ); + const responses = await Promise.allSettled( promises ); + + const results = { + errors: [], + successes: [], + }; + + responses.forEach( ( result, index ) => { + if ( result.status === 'fulfilled' ) { + const response = result.value; + if ( response.id ) { + results.successes.push( response ); + } else { + results.errors.push( { + data: fontFacesData[ index ], + message: `Error: ${ response.message }`, + } ); + } + } else { + // Handle network errors or other fetch-related errors + results.errors.push( { + data: fontFacesData[ index ], + error: `Fetch error: ${ result.reason }`, + } ); + } + } ); + + return results; } /* diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js deleted file mode 100644 index 9f38903c89759b..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Internal dependencies - */ -import { makeFormDataFromFontFamily } from '../index'; - -/* global File */ - -describe( 'makeFormDataFromFontFamily', () => { - it( 'should process fontFamilies and return FormData', () => { - const mockFontFamily = { - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - fontFace: [ - { - file: new File( [ 'content' ], 'test-font1.woff2' ), - fontWeight: '500', - fontStyle: 'normal', - }, - { - file: new File( [ 'content' ], 'test-font2.woff2' ), - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }; - - const formData = makeFormDataFromFontFamily( mockFontFamily ); - - expect( formData instanceof FormData ).toBeTruthy(); - - // Check if files are added correctly - expect( formData.get( 'file-0' ).name ).toBe( 'test-font1.woff2' ); - expect( formData.get( 'file-1' ).name ).toBe( 'test-font2.woff2' ); - - // Check if 'fontFamilies' key in FormData is correct - const expectedFontFamily = { - fontFace: [ - { - fontWeight: '500', - fontStyle: 'normal', - uploadedFile: 'file-0', - }, - { - fontWeight: '400', - fontStyle: 'normal', - uploadedFile: 'file-1', - }, - ], - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - }; - expect( JSON.parse( formData.get( 'font_family_settings' ) ) ).toEqual( - expectedFontFamily - ); - } ); -} ); From 9ebc25f710b7f94e610250e41a6bf9751ba55555 Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Wed, 17 Jan 2024 12:56:26 -0500 Subject: [PATCH 08/16] Cleanup/font library view error handling (#57926) * Add batchInstallFontFaces function and related plumbing. * Fix resolver name. * Add embedding and rebuild theme.json settings for fontFamily. * Handle responses directly, add to collection before activating. Remove unused test. * Remove getIntersectingFontFaces. * Check for existing font family before installing. * Reference src, not uploadedFile key. Co-authored-by: Matias Benedetto * Check for existing font family using GET /font-families?slug=. * Filter already installed font faces (determined by matching fontWeight AND fontStyle) * moved response processing into the resolver for fetchGetFontFamilyBySlug * Moved response processing for font family installation to the resolver * Refactored font face installation process to handle errors more cleanly * Cleanup error handling for font library view * Add i18n function to error messages * Add TODO comment for uninstall notice --------- Co-authored-by: Jeff Ong Co-authored-by: Matias Benedetto Co-authored-by: Sarah Norris --- .../font-library-modal/context.js | 145 +++++----- .../font-library-modal/font-collection.js | 16 +- .../font-library-modal/installed-fonts.js | 6 +- .../font-library-modal/local-fonts.js | 18 +- .../font-library-modal/resolvers.js | 30 +- .../utils/get-intersecting-font-faces.js | 58 ---- .../utils/get-notice-from-response.js | 62 ---- .../font-library-modal/utils/index.js | 22 +- .../test/getIntersectingFontFaces.spec.js | 271 ------------------ 9 files changed, 143 insertions(+), 485 deletions(-) delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index c6e37d45c689ec..078d11e235c043 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -9,6 +9,7 @@ import { useEntityRecords, store as coreStore, } from '@wordpress/core-data'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -30,6 +31,7 @@ import { makeFontFacesFormData, makeFontFamilyFormData, batchInstallFontFaces, + checkFontFaceInstalled, } from './utils'; import { toggleFont } from './utils/toggleFont'; @@ -201,109 +203,94 @@ function FontLibraryProvider( { children } ) { return getActivatedFontsOutline( source )[ slug ] || []; }; - async function installFont( font ) { + async function installFont( fontFamilyToInstall ) { setIsInstalling( true ); try { - // Get the ID of the font family post, if it is already installed. + // Get the font family if it already exists. let installedFontFamily = await fetchGetFontFamilyBySlug( - font.slug - ) - .then( ( response ) => { - if ( ! response || response.length === 0 ) { - return null; - } - const fontFamilyPost = response[ 0 ]; - return { - id: fontFamilyPost.id, - ...fontFamilyPost.font_family_settings, - fontFace: - fontFamilyPost?._embedded?.font_faces.map( - ( face ) => face.font_face_settings - ) || [], - }; - } ) - .catch( ( e ) => { - // eslint-disable-next-line no-console - console.error( e ); - return null; - } ); + fontFamilyToInstall.slug + ); - // Otherwise, install it. + // Otherwise create it. if ( ! installedFontFamily ) { - const fontFamilyFormData = makeFontFamilyFormData( font ); // Prepare font family form data to install. installedFontFamily = await fetchInstallFontFamily( - fontFamilyFormData - ) - .then( ( response ) => { - return { - id: response.id, - ...response.font_face_settings, - fontFace: [], - }; - } ) - .catch( ( e ) => { - throw Error( e.message ); - } ); + makeFontFamilyFormData( fontFamilyToInstall ) + ); } - // Filter Font Faces that have already been installed - // We determine that by comparing the fontWeight and fontStyle - font.fontFace = font.fontFace.filter( ( fontFaceToInstall ) => { - return ( - -1 === - installedFontFamily.fontFace.findIndex( - ( installedFontFace ) => { - return ( - installedFontFace.fontWeight === - fontFaceToInstall.fontWeight && - installedFontFace.fontStyle === - fontFaceToInstall.fontStyle - ); - } + // Collect font faces that have already been installed (to be activated later) + const alreadyInstalledFontFaces = + installedFontFamily.fontFace.filter( ( fontFaceToInstall ) => + checkFontFaceInstalled( + fontFaceToInstall, + fontFamilyToInstall.fontFace ) ); - } ); - - if ( font.fontFace.length === 0 ) { - // Looks like we're only trying to install fonts that are already installed. - // Let's not do that. - // TODO: Exit with an error message? - return { - errors: [ 'All font faces are already installed' ], - }; - } - // Prepare font faces form data to install. - const fontFacesFormData = makeFontFacesFormData( font ); + // Filter out Font Faces that have already been installed (so that they are not re-installed) + fontFamilyToInstall.fontFace = fontFamilyToInstall.fontFace.filter( + ( fontFaceToInstall ) => + ! checkFontFaceInstalled( + fontFaceToInstall, + installedFontFamily.fontFace + ) + ); // Install the fonts (upload the font files to the server and create the post in the database). - const response = await batchInstallFontFaces( - installedFontFamily.id, - fontFacesFormData + let sucessfullyInstalledFontFaces = []; + let unsucessfullyInstalledFontFaces = []; + if ( fontFamilyToInstall.fontFace.length > 0 ) { + const response = await batchInstallFontFaces( + installedFontFamily.id, + makeFontFacesFormData( fontFamilyToInstall ) + ); + sucessfullyInstalledFontFaces = response?.successes; + unsucessfullyInstalledFontFaces = response?.errors; + } + + const detailedErrorMessage = unsucessfullyInstalledFontFaces.reduce( + ( errorMessageCollection, error ) => { + return `${ errorMessageCollection } ${ error.message }`; + }, + '' ); - const fontFacesInstalled = response?.successes || []; + // If there were no successes and nothing already installed then we don't need to activate anything and can bounce now. + if ( + sucessfullyInstalledFontFaces.length === 0 && + alreadyInstalledFontFaces.length === 0 + ) { + throw new Error( + __( 'No font faces were installed. ' ) + + detailedErrorMessage + ); + } - // Rebuild fontFace settings - font.fontFace = - fontFacesInstalled.map( ( face ) => { - return face.font_face_settings; - } ) || []; + // Use the sucessfully installed font faces + // As well as any font faces that were already installed (those will be activated) + fontFamilyToInstall.fontFace = [ + ...sucessfullyInstalledFontFaces, + ...alreadyInstalledFontFaces, + ]; // Activate the font family (add the font family to the global styles). - activateCustomFontFamilies( [ font ] ); + activateCustomFontFamilies( [ fontFamilyToInstall ] ); + // Save the global styles to the database. saveSpecifiedEntityEdits( 'root', 'globalStyles', globalStylesId, [ 'settings.typography.fontFamilies', ] ); + refreshLibrary(); - return response; - } catch ( error ) { - return { - errors: [ error ], - }; + if ( unsucessfullyInstalledFontFaces.length > 0 ) { + throw new Error( + __( + 'Some font faces were installed. There were some errors. ' + ) + detailedErrorMessage + ); + } } finally { setIsInstalling( false ); } @@ -324,7 +311,7 @@ function FontLibraryProvider( { children } ) { [ 'settings.typography.fontFamilies' ] ); } - // Refresh the library (the the library font families from database). + // Refresh the library (the library font families from database). refreshLibrary(); return response; } catch ( error ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index f7f33032f1e3f5..5b6eeb2481e7a4 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -30,7 +30,6 @@ import CollectionFontDetails from './collection-font-details'; import { toggleFont } from './utils/toggleFont'; import { getFontsOutline } from './utils/fonts-outline'; import GoogleFontsConfirmDialog from './google-fonts-confirm-dialog'; -import { getNoticeFromInstallResponse } from './utils/get-notice-from-response'; import { downloadFontFaceAsset } from './utils'; const DEFAULT_CATEGORY = { @@ -182,9 +181,18 @@ function FontCollection( { id } ) { return; } - const response = await installFont( fontFamily ); - const installNotice = getNoticeFromInstallResponse( response ); - setNotice( installNotice ); + try { + await installFont( fontFamily ); + setNotice( { + type: 'success', + message: __( 'Fonts were installed successfully.' ), + } ); + } catch ( error ) { + setNotice( { + type: 'error', + message: error.message, + } ); + } resetFontsToInstall(); }; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index d493a2a297b18b..0c481f6e4d8ca8 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -22,7 +22,6 @@ import FontsGrid from './fonts-grid'; import LibraryFontDetails from './library-font-details'; import LibraryFontCard from './library-font-card'; import ConfirmDeleteDialog from './confirm-delete-dialog'; -import { getNoticeFromUninstallResponse } from './utils/get-notice-from-response'; import { unlock } from '../../../lock-unlock'; const { ProgressBar } = unlock( componentsPrivateApis ); @@ -50,8 +49,9 @@ function InstalledFonts() { const handleConfirmUninstall = async () => { const response = await uninstallFont( libraryFontSelected ); - const uninstallNotice = getNoticeFromUninstallResponse( response ); - setNotice( uninstallNotice ); + // TODO: Refactor uninstall notices + // const uninstallNotice = getNoticeFromUninstallResponse( response ); + // setNotice( uninstallNotice ); // If the font was succesfully uninstalled it is unselected if ( ! response?.errors?.length ) { handleUnselectFont(); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js index d4221b420cb613..a77b524dddbed0 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js @@ -23,7 +23,6 @@ import { FontLibraryContext } from './context'; import { Font } from '../../../../lib/lib-font.browser'; import makeFamiliesFromFaces from './utils/make-families-from-faces'; import { loadFontFaceInBrowser } from './utils'; -import { getNoticeFromInstallResponse } from './utils/get-notice-from-response'; import { unlock } from '../../../lock-unlock'; const { ProgressBar } = unlock( componentsPrivateApis ); @@ -161,12 +160,23 @@ function LocalFonts() { 'Variants from only one font family can be uploaded at a time.' ), } ); + setIsUploading( false ); return; } - const response = await installFont( fontFamilies[ 0 ] ); - const installNotice = getNoticeFromInstallResponse( response ); - setNotice( installNotice ); + try { + await installFont( fontFamilies[ 0 ] ); + setNotice( { + type: 'success', + message: __( 'Fonts were installed successfully.' ), + } ); + } catch ( error ) { + setNotice( { + type: 'error', + message: error, + } ); + } + setIsUploading( false ); }; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index 08e37dc7ee95fb..df10904b75026f 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -13,7 +13,13 @@ export async function fetchInstallFontFamily( data ) { method: 'POST', body: data, }; - return apiFetch( config ); + return apiFetch( config ).then( ( response ) => { + return { + id: response.id, + ...response.font_face_settings, + fontFace: [], + }; + } ); } export async function fetchInstallFontFace( fontFamilyId, data ) { @@ -22,7 +28,12 @@ export async function fetchInstallFontFace( fontFamilyId, data ) { method: 'POST', body: data, }; - return apiFetch( config ); + return apiFetch( config ).then( ( response ) => { + return { + id: response.id, + ...response.font_face_settings, + }; + } ); } export async function fetchGetFontFamilyBySlug( slug ) { @@ -30,7 +41,20 @@ export async function fetchGetFontFamilyBySlug( slug ) { path: `/wp/v2/font-families?slug=${ slug }&_embed=true`, method: 'GET', }; - return apiFetch( config ); + return apiFetch( config ).then( ( response ) => { + if ( ! response || response.length === 0 ) { + return null; + } + const fontFamilyPost = response[ 0 ]; + return { + id: fontFamilyPost.id, + ...fontFamilyPost.font_family_settings, + fontFace: + fontFamilyPost?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || [], + }; + } ); } export async function fetchUninstallFonts( fonts ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js deleted file mode 100644 index e21e72c58ed533..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Retrieves intersecting font faces between two sets of fonts. - * - * For each font in the `incoming` list, the function checks for a corresponding match - * in the `existing` list based on the `slug` property. If a match is found and both - * have `fontFace` properties, it further narrows down to matching font faces based on - * the `fontWeight` and `fontStyle`. The result includes the properties of the matched - * existing font but only with intersecting font faces. - * - * @param {Array.<{ slug: string, fontFace?: Array.<{ fontWeight: string, fontStyle: string }> }>} incoming - The list of fonts to compare. - * @param {Array.<{ slug: string, fontFace?: Array.<{ fontWeight: string, fontStyle: string }> }>} existing - The reference list of fonts. - * - * @return {Array.<{ slug: string, fontFace?: Array.<{ fontWeight: string, fontStyle: string }> }>} An array of fonts from the `existing` list with intersecting font faces. - * - * @example - * const incomingFonts = [ - * { slug: 'arial', fontFace: [{ fontWeight: '400', fontStyle: 'normal' }] }, - * { slug: 'times-new', fontFace: [{ fontWeight: '700', fontStyle: 'italic' }] } - * ]; - * - * const existingFonts = [ - * { slug: 'arial', fontFace: [{ fontWeight: '400', fontStyle: 'normal' }, { fontWeight: '700', fontStyle: 'italic' }] }, - * { slug: 'helvetica', fontFace: [{ fontWeight: '400', fontStyle: 'normal' }] } - * ]; - * - * getIntersectingFontFaces(incomingFonts, existingFonts); - * // Returns: - * // [{ slug: 'arial', fontFace: [{ fontWeight: '400', fontStyle: 'normal' }] }] - */ -export default function getIntersectingFontFaces( incoming, existing ) { - const matches = []; - - for ( const incomingFont of incoming ) { - const existingFont = existing.find( - ( f ) => f.slug === incomingFont.slug - ); - - if ( existingFont ) { - if ( incomingFont?.fontFace ) { - const matchingFaces = incomingFont.fontFace.filter( - ( face ) => { - return ( existingFont?.fontFace || [] ).find( ( f ) => { - return ( - f.fontWeight === face.fontWeight && - f.fontStyle === face.fontStyle - ); - } ); - } - ); - matches.push( { ...incomingFont, fontFace: matchingFaces } ); - } else { - matches.push( incomingFont ); - } - } - } - - return matches; -} diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js deleted file mode 100644 index b22bd0afe23248..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -export function getNoticeFromInstallResponse( response ) { - const { errors = [], successes = [] } = response; - // Everything failed. - if ( errors.length && ! successes.length ) { - return { - type: 'error', - message: __( 'Error installing the fonts.' ), - }; - } - - // Eveerything succeeded. - if ( ! errors.length && successes.length ) { - return { - type: 'success', - message: __( 'Fonts were installed successfully.' ), - }; - } - - // Some succeeded, some failed. - if ( errors.length && successes.length ) { - return { - type: 'warning', - message: __( - 'Some fonts were installed successfully and some failed.' - ), - }; - } -} - -export function getNoticeFromUninstallResponse( response ) { - const { errors = [], successes = [] } = response; - // Everything failed. - if ( errors.length && ! successes.length ) { - return { - type: 'error', - message: __( 'Error uninstalling the fonts.' ), - }; - } - - // Everything succeeded. - if ( ! errors.length && successes.length ) { - return { - type: 'success', - message: __( 'Fonts were uninstalled successfully.' ), - }; - } - - // Some succeeded, some failed. - if ( errors.length && successes.length ) { - return { - type: 'warning', - message: __( - 'Some fonts were uninstalled successfully and some failed.' - ), - }; - } -} diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 98b6375740e5b4..1adc8847f15171 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -208,7 +208,7 @@ export async function batchInstallFontFaces( fontFamilyId, fontFacesData ) { // Handle network errors or other fetch-related errors results.errors.push( { data: fontFacesData[ index ], - error: `Fetch error: ${ result.reason }`, + message: `Fetch error: ${ result.reason.message }`, } ); } } ); @@ -245,3 +245,23 @@ export async function downloadFontFaceAsset( url ) { throw error; } ); } + +/* + * Determine if a given Font Face is present in a given collection. + * We determine that a font face has been installed by comparing the fontWeight and fontStyle + * + * @param {Object} fontFace The Font Face to seek + * @param {Array} collection The Collection to seek in + * @returns True if the font face is found in the collection. Otherwise False. + */ +export function checkFontFaceInstalled( fontFace, collection ) { + return ( + -1 !== + collection.findIndex( ( collectionFontFace ) => { + return ( + collectionFontFace.fontWeight === fontFace.fontWeight && + collectionFontFace.fontStyle === fontFace.fontStyle + ); + } ) + ); +} diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js deleted file mode 100644 index 9899005ad65b89..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Internal dependencies - */ -import getIntersectingFontFaces from '../get-intersecting-font-faces'; - -describe( 'getIntersectingFontFaces', () => { - it( 'returns matching font faces for matching font family', () => { - const incomingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const existingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - - expect( result ).toEqual( incomingFontFamilies ); - } ); - - it( 'returns empty array when there is no match', () => { - const incomingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const existingFontFamilies = [ - { - slug: 'montserrat', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - - expect( result ).toEqual( [] ); - } ); - - it( 'returns matching font faces', () => { - const incomingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - { - fontWeight: '700', - fontStyle: 'italic', - }, - ], - }, - { - slug: 'times', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const existingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - { - fontWeight: '800', - fontStyle: 'italic', - }, - { - fontWeight: '900', - fontStyle: 'italic', - }, - ], - }, - ]; - - const expectedOutput = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - - expect( result ).toEqual( expectedOutput ); - } ); - - it( 'returns empty array when the first list is empty', () => { - const incomingFontFamilies = []; - - const existingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - - expect( result ).toEqual( [] ); - } ); - - it( 'returns empty array when the second list is empty', () => { - const incomingFontFamilies = [ - { - slug: 'lobster', - fontFace: [ - { - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const existingFontFamilies = []; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - - expect( result ).toEqual( [] ); - } ); - - it( 'returns intersecting font family when there are no fonfaces', () => { - const incomingFontFamilies = [ - { - slug: 'piazzolla', - fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], - }, - { - slug: 'lobster', - }, - ]; - - const existingFontFamilies = [ - { - slug: 'lobster', - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - - expect( result ).toEqual( existingFontFamilies ); - } ); - - it( 'returns intersecting if there is an intended font face and is not present in the returning it should not be returned', () => { - const incomingFontFamilies = [ - { - slug: 'piazzolla', - fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], - }, - { - slug: 'lobster', - fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], - }, - ]; - - const existingFontFamilies = [ - { - slug: 'lobster', - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - const expected = [ - { - slug: 'lobster', - fontFace: [], - }, - ]; - expect( result ).toEqual( expected ); - } ); - - it( 'updates font family definition using the incoming data', () => { - const incomingFontFamilies = [ - { - slug: 'gothic-a1', - fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], - fontFamily: "'Gothic A1', serif", - }, - ]; - - const existingFontFamilies = [ - { - slug: 'gothic-a1', - fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], - fontFamily: 'Gothic A1, serif', - }, - ]; - - const result = getIntersectingFontFaces( - incomingFontFamilies, - existingFontFamilies - ); - const expected = [ - { - slug: 'gothic-a1', - fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], - fontFamily: "'Gothic A1', serif", - }, - ]; - expect( result ).toEqual( expected ); - } ); -} ); From 8a5efd40439d7d61035548ab8419fc89591094ae Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Wed, 17 Jan 2024 16:37:41 -0600 Subject: [PATCH 09/16] Font Faces endpoint: prevent creating font faces with duplicate settings (#57903) --- .../class-wp-font-family-utils.php | 82 ++++++++++++++++++- .../class-wp-rest-font-faces-controller.php | 28 ++++++- .../wpFontFamilyUtils/formatFontFamily.php | 6 +- .../wpFontFamilyUtils/getFontFaceSlug.php | 81 ++++++++++++++++++ .../wpRestFontFacesController.php | 62 ++++++++++---- .../wpRestFontFamiliesController.php | 15 ++-- 6 files changed, 247 insertions(+), 27 deletions(-) create mode 100644 phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFontFaceSlug.php diff --git a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php index 35e6856e50aad8..71a3bb262e6005 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php @@ -91,7 +91,7 @@ public static function format_font_family( $font_family ) { function ( $family ) { $trimmed = trim( $family ); if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) { - return "'" . $trimmed . "'"; + return '"' . $trimmed . '"'; } return $trimmed; }, @@ -107,4 +107,84 @@ function ( $family ) { return $font_family; } + + /** + * Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF` + * + * Used for comparison with other font faces in the same family, to prevent duplicates + * that would both match according the CSS font matching spec. Uses only simple case-insensitive + * matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or + * unicode ranges. + * + * @since 6.5.0 + * + * @link https://drafts.csswg.org/css-fonts/#font-style-matching + * + * @param array $settings { + * Font face settings. + * + * @type string $fontFamily Font family name. + * @type string $fontStyle Optional font style, defaults to 'normal'. + * @type string $fontWeight Optional font weight, defaults to 400. + * @type string $fontStretch Optional font stretch, defaults to '100%'. + * @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'. + * } + * @return string Font face slug. + */ + public static function get_font_face_slug( $settings ) { + $settings = wp_parse_args( + $settings, + array( + 'fontFamily' => '', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'fontStretch' => '100%', + 'unicodeRange' => 'U+0-10FFFF', + ) + ); + + // Convert all values to lowercase for comparison. + // Font family names may use multibyte characters. + $font_family = mb_strtolower( $settings['fontFamily'] ); + $font_style = strtolower( $settings['fontStyle'] ); + $font_weight = strtolower( $settings['fontWeight'] ); + $font_stretch = strtolower( $settings['fontStretch'] ); + $unicode_range = strtoupper( $settings['unicodeRange'] ); + + // Convert weight keywords to numeric strings. + $font_weight = str_replace( 'normal', '400', $font_weight ); + $font_weight = str_replace( 'bold', '700', $font_weight ); + + // Convert stretch keywords to numeric strings. + $font_stretch_map = array( + 'ultra-condensed' => '50%', + 'extra-condensed' => '62.5%', + 'condensed' => '75%', + 'semi-condensed' => '87.5%', + 'normal' => '100%', + 'semi-expanded' => '112.5%', + 'expanded' => '125%', + 'extra-expanded' => '150%', + 'untra-expanded' => '200%', + ); + $font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch ); + + $slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range ); + + $slug_elements = array_map( + function ( $elem ) { + // Remove quotes to normalize font-family names, and ';' to use as a separator. + $elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) ); + + // Normalize comma separated lists by removing whitespace in between items, + // but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts). + // CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE, + // which by default are all matched by \s in PHP. + return preg_replace( '/,\s+/', ',', $elem ); + }, + $slug_elements + ); + + return join( ';', $slug_elements ); + } } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php index 7f4fe653a28548..a5d27fda072f56 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -295,6 +295,22 @@ public function create_item( $request ) { $settings = $request->get_param( 'font_face_settings' ); $file_params = $request->get_file_params(); + // Check that the necessary font face properties are unique. + $existing_font_face = get_posts( + array( + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'title' => WP_Font_Family_Utils::get_font_face_slug( $settings ), + ) + ); + if ( ! empty( $existing_font_face ) ) { + return new WP_Error( + 'rest_duplicate_font_face', + __( 'A font face matching those settings already exists.', 'gutenberg' ), + 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'; @@ -648,11 +664,15 @@ protected function prepare_item_for_database( $request ) { // 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_Family_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 = $settings['fontFamily']; - $prepared_post->post_name = sanitize_title( $settings['fontFamily'] ); + $prepared_post->post_title = $title; + $prepared_post->post_name = sanitize_title( $title ); $prepared_post->post_content = wp_json_encode( $settings ); return $prepared_post; @@ -751,10 +771,10 @@ protected function get_settings_from_post( $post ) { // Provide required, empty settings if needed. if ( null === $settings ) { $settings = array( - 'src' => array(), + 'fontFamily' => '', + 'src' => array(), ); } - $settings['fontFamily'] = $post->post_title ?? ''; // Only return the properties defined in the schema. return array_intersect_key( $settings, $properties ); diff --git a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/formatFontFamily.php b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/formatFontFamily.php index 4f247c5219febb..19987010d80a7e 100644 --- a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/formatFontFamily.php +++ b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/formatFontFamily.php @@ -36,11 +36,11 @@ public function data_should_format_font_family() { return array( 'data_families_with_spaces_and_numbers' => array( 'font_family' => 'Rock 3D , Open Sans,serif', - 'expected' => "'Rock 3D', 'Open Sans', serif", + 'expected' => '"Rock 3D", "Open Sans", serif', ), 'data_single_font_family' => array( 'font_family' => 'Rock 3D', - 'expected' => "'Rock 3D'", + 'expected' => '"Rock 3D"', ), 'data_no_spaces' => array( 'font_family' => 'Rock3D', @@ -48,7 +48,7 @@ public function data_should_format_font_family() { ), 'data_many_spaces_and_existing_quotes' => array( 'font_family' => 'Rock 3D serif, serif,sans-serif, "Open Sans"', - 'expected' => "'Rock 3D serif', serif, sans-serif, \"Open Sans\"", + 'expected' => '"Rock 3D serif", serif, sans-serif, "Open Sans"', ), 'data_empty_family' => array( 'font_family' => ' ', diff --git a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFontFaceSlug.php b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFontFaceSlug.php new file mode 100644 index 00000000000000..1f87d0d2fd5a11 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFontFaceSlug.php @@ -0,0 +1,81 @@ +assertSame( $expected_slug, $slug ); + } + + public function data_get_font_face_slug_normalizes_values() { + return array( + 'Sets defaults' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Converts normal weight to 400' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => 'normal', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Converts bold weight to 700' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => 'bold', + ), + 'expected_slug' => 'open sans;normal;700;100%;U+0-10FFFF', + ), + 'Converts normal font-stretch to 100%' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontStretch' => 'normal', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes double quotes from fontFamilies' => array( + 'settings' => array( + 'fontFamily' => '"Open Sans"', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes single quotes from fontFamilies' => array( + 'settings' => array( + 'fontFamily' => "'Open Sans'", + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes spaces between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans, serif', + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + 'Removes tabs between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => "Open Sans,\tserif", + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + 'Removes new lines between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => "Open Sans,\nserif", + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index d248394e611dbf..a53289735c4297 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -30,14 +30,14 @@ class WP_REST_Font_Faces_Controller_Test extends WP_Test_REST_Controller_Testcas public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); self::$other_font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); - self::$font_face_id1 = self::create_font_face_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( @@ -69,13 +69,14 @@ public static function wpTearDownAfterClass() { public static function create_font_face_post( $parent_id, $settings = array() ) { $settings = array_merge( self::$default_settings, $settings ); + $title = WP_Font_Family_Utils::get_font_face_slug( $settings ); return self::factory()->post->create( wp_slash( array( 'post_type' => 'wp_font_face', 'post_status' => 'publish', - 'post_title' => $settings['fontFamily'], - 'post_name' => sanitize_title( $settings['fontFamily'] ), + 'post_title' => $title, + 'post_name' => sanitize_title( $title ), 'post_content' => wp_json_encode( $settings ), 'post_parent' => $parent_id, ) @@ -200,8 +201,8 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { ); $empty_settings = array( - 'src' => array(), 'fontFamily' => '', + 'src' => array(), ); wp_set_current_user( self::$admin_id ); @@ -291,7 +292,7 @@ public function test_create_item() { wp_json_encode( array( 'fontFamily' => '"Open Sans"', - 'fontWeight' => '400', + 'fontWeight' => '200', 'fontStyle' => 'normal', 'src' => array_keys( $files )[0], ) @@ -312,7 +313,7 @@ public function test_create_item() { $settings, array( 'fontFamily' => '"Open Sans"', - 'fontWeight' => '400', + 'fontWeight' => '200', 'fontStyle' => 'normal', ) ); @@ -336,7 +337,7 @@ public function test_create_item_with_multiple_font_files() { wp_json_encode( array( 'fontFamily' => '"Open Sans"', - 'fontWeight' => '400', + 'fontWeight' => '200', 'fontStyle' => 'normal', 'src' => array_keys( $files ), ) @@ -382,7 +383,13 @@ public function test_create_item_invalid_file_type() { $request->set_param( 'font_face_settings', wp_json_encode( - array_merge( self::$default_settings, array( 'src' => array_keys( $files )[0] ) ) + array_merge( + self::$default_settings, + array( + 'fontWeight' => '200', + 'src' => array_keys( $files )[0], + ) + ) ) ); $request->set_file_params( $files ); @@ -404,7 +411,7 @@ public function test_create_item_with_url_src() { wp_json_encode( array( 'fontFamily' => '"Open Sans"', - 'fontWeight' => '400', + 'fontWeight' => '200', 'fontStyle' => 'normal', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', ) @@ -458,6 +465,32 @@ public function test_create_item_with_all_properties() { wp_delete_post( $data['id'], true ); } + /** + * @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' ), + ); + $font_face_id = 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 ); + $expected_message = 'A font face matching those settings already exists.'; + $message = $response->as_error()->get_error_messages()[0]; + $this->assertSame( $expected_message, $message ); + + wp_delete_post( $font_face_id, true ); + } + /** * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_request */ @@ -469,6 +502,7 @@ public function test_create_item_default_theme_json_version() { wp_json_encode( array( 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', ) ) @@ -602,7 +636,7 @@ public function test_create_item_invalid_file_src() { /** * @dataProvider data_create_item_santize_font_family * - * @covers WP_REST_Font_Families_Controller::update_item + * @covers WP_REST_Font_Face_Controller::sanitize_font_face_settings */ public function test_create_item_santize_font_family( $font_family_setting, $expected ) { $settings = array_merge( self::$default_settings, array( 'fontFamily' => $font_family_setting ) ); @@ -619,9 +653,9 @@ public function test_create_item_santize_font_family( $font_family_setting, $exp public function data_create_item_santize_font_family() { return array( - array( 'Libre Barcode 128 Text', "'Libre Barcode 128 Text'" ), - array( 'B612 Mono', "'B612 Mono'" ), - array( 'Open Sans, Noto Sans, sans-serif', "'Open Sans', 'Noto Sans', sans-serif" ), + array( 'Libre Barcode 128 Text', '"Libre Barcode 128 Text"' ), + array( 'B612 Mono', '"B612 Mono"' ), + array( 'Open Sans, Noto Sans, sans-serif', '"Open Sans", "Noto Sans", sans-serif' ), ); } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index b11e0de3d2cc25..e0fe5e0c8cb93e 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -548,7 +548,7 @@ public function data_update_item_individual_settings() { /** * @dataProvider data_update_item_santize_font_family * - * @covers WP_REST_Font_Families_Controller::update_item + * @covers WP_REST_Font_Families_Controller::sanitize_font_face_settings */ public function test_update_item_santize_font_family( $font_family_setting, $expected ) { wp_set_current_user( self::$admin_id ); @@ -567,9 +567,9 @@ public function test_update_item_santize_font_family( $font_family_setting, $exp public function data_update_item_santize_font_family() { return array( - array( 'Libre Barcode 128 Text', "'Libre Barcode 128 Text'" ), - array( 'B612 Mono', "'B612 Mono'" ), - array( 'Open Sans, Noto Sans, sans-serif', "'Open Sans', 'Noto Sans', sans-serif" ), + array( 'Libre Barcode 128 Text', '"Libre Barcode 128 Text"' ), + array( 'B612 Mono', '"B612 Mono"' ), + array( 'Open Sans, Noto Sans, sans-serif', '"Open Sans", "Noto Sans", sans-serif' ), ); } @@ -767,10 +767,15 @@ protected function check_font_family_data( $data, $post_id, $links ) { 'fields' => 'ids', 'post_parent' => $post_id, 'post_type' => 'wp_font_face', + 'order' => 'ASC', + 'orderby' => 'ID', ) ); $this->assertArrayHasKey( 'font_faces', $data ); - $this->assertSame( $font_face_ids, $data['font_faces'] ); + + foreach ( $font_face_ids as $font_face_id ) { + $this->assertContains( $font_face_id, $data['font_faces'] ); + } $this->assertArrayHasKey( 'font_family_settings', $data ); $settings = $data['font_family_settings']; From 3a739bc2a46230635ccef925a6b1dbb65f413a29 Mon Sep 17 00:00:00 2001 From: Sarah Norris <1645628+mikachan@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:04:53 +0000 Subject: [PATCH 10/16] Font Library: Update uninstall/delete on client side (#57932) * Fix delete endpoint * Update fetchUninstallFontFamily to match new format * Update uninstallFont * Add uninstall notice back in * Tidy up comments * Re-word uninstall notices * Add spacing to error message * Refactored how font family values were processed so they would retain their id, which is now used to delete a font family without fetching data via slug * Rename uninstallFont to uninstallFontFamily * Throw uninstall errors rather than returning them --------- Co-authored-by: Jason Crist --- ...class-wp-rest-font-families-controller.php | 2 + .../font-library-modal/context.js | 49 ++++++++++++------- .../font-library-modal/installed-fonts.js | 25 +++++++--- .../font-library-modal/resolvers.js | 8 +-- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index 89bc88020e3330..a48c9cbe1a7763 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -263,6 +263,8 @@ public function delete_item( $request ) { foreach ( $this->get_font_face_ids( $font_family_id ) as $font_face_id ) { wp_delete_post( $font_face_id, true ); } + + return $deleted; } /** diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 078d11e235c043..c653d0a8b03626 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -17,7 +17,7 @@ import { __ } from '@wordpress/i18n'; import { fetchGetFontFamilyBySlug, fetchInstallFontFamily, - fetchUninstallFonts, + fetchUninstallFontFamily, fetchFontCollections, fetchFontCollection, } from './resolvers'; @@ -70,12 +70,15 @@ function FontLibraryProvider( { children } ) { } ); const libraryFonts = - ( libraryPosts || [] ).map( ( post ) => { - post.font_family_settings.fontFace = - post?._embedded?.font_faces.map( - ( face ) => face.font_face_settings - ) || []; - return post.font_family_settings; + ( libraryPosts || [] ).map( ( fontFamilyPost ) => { + return { + id: fontFamilyPost.id, + ...fontFamilyPost.font_family_settings, + fontFace: + fontFamilyPost?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || [], + }; } ) || []; // Global Styles (settings) font families @@ -296,13 +299,18 @@ function FontLibraryProvider( { children } ) { } } - async function uninstallFont( font ) { + async function uninstallFontFamily( fontFamilyToUninstall ) { try { - // Uninstall the font (remove the font files from the server and the post from the database). - const response = await fetchUninstallFonts( [ font ] ); - // Deactivate the font family (remove the font family from the global styles). - if ( 0 === response.errors.length ) { - deactivateFontFamily( font ); + // Uninstall the font family. + // (Removes the font files from the server and the posts from the database). + const uninstalledFontFamily = await fetchUninstallFontFamily( + fontFamilyToUninstall.id + ); + + // Deactivate the font family if delete request is successful + // (Removes the font family from the global styles). + if ( uninstalledFontFamily.deleted ) { + deactivateFontFamily( fontFamilyToUninstall ); // Save the global styles to the database. await saveSpecifiedEntityEdits( 'root', @@ -311,15 +319,18 @@ function FontLibraryProvider( { children } ) { [ 'settings.typography.fontFamilies' ] ); } + // Refresh the library (the library font families from database). refreshLibrary(); - return response; + + return uninstalledFontFamily; } catch ( error ) { // eslint-disable-next-line no-console - console.error( error ); - return { - errors: [ error ], - }; + console.error( + `There was an error uninstalling the font family:`, + error + ); + throw error; } } @@ -431,7 +442,7 @@ function FontLibraryProvider( { children } ) { getFontFacesActivated, loadFontFaceAsset, installFont, - uninstallFont, + uninstallFontFamily, toggleActivateFont, getAvailableFontsOutline, modalTabOpen, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 0c481f6e4d8ca8..714f91f4bc04c7 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -32,7 +32,7 @@ function InstalledFonts() { baseThemeFonts, handleSetLibraryFontSelected, refreshLibrary, - uninstallFont, + uninstallFontFamily, isResolvingLibrary, } = useContext( FontLibraryContext ); const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState( false ); @@ -48,15 +48,24 @@ function InstalledFonts() { const [ notice, setNotice ] = useState( null ); const handleConfirmUninstall = async () => { - const response = await uninstallFont( libraryFontSelected ); - // TODO: Refactor uninstall notices - // const uninstallNotice = getNoticeFromUninstallResponse( response ); - // setNotice( uninstallNotice ); - // If the font was succesfully uninstalled it is unselected - if ( ! response?.errors?.length ) { + try { + await uninstallFontFamily( libraryFontSelected ); + setNotice( { + type: 'success', + message: __( 'Font family uninstalled successfully.' ), + } ); + + // If the font was succesfully uninstalled it is unselected. handleUnselectFont(); + setIsConfirmDeleteOpen( false ); + } catch ( error ) { + setNotice( { + type: 'error', + message: + __( 'There was an error uninstalling the font family. ' ) + + error.message, + } ); } - setIsConfirmDeleteOpen( false ); }; const handleUninstallClick = async () => { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index df10904b75026f..baaa6985995538 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -57,14 +57,10 @@ export async function fetchGetFontFamilyBySlug( slug ) { } ); } -export async function fetchUninstallFonts( fonts ) { - const data = { - font_families: fonts, - }; +export async function fetchUninstallFontFamily( fontFamilyId ) { const config = { - path: '/wp/v2/font-families', + path: `/wp/v2/font-families/${ fontFamilyId }?force=true`, method: 'DELETE', - data, }; return apiFetch( config ); } From c6e0fbb385afebd398d52190c96b8c4feab464cd Mon Sep 17 00:00:00 2001 From: Sarah Norris <1645628+mikachan@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:11:31 +0000 Subject: [PATCH 11/16] Update packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js Co-authored-by: Jonny Harris --- .../components/global-styles/font-library-modal/local-fonts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js index a77b524dddbed0..145f4164a87607 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js @@ -173,7 +173,7 @@ function LocalFonts() { } catch ( error ) { setNotice( { type: 'error', - message: error, + message: error.message, } ); } From a666bb5313f45a6fa219cd7f37c884307831981f Mon Sep 17 00:00:00 2001 From: Sarah Norris <1645628+mikachan@users.noreply.github.com> Date: Thu, 18 Jan 2024 14:39:24 +0000 Subject: [PATCH 12/16] Font Library: address JS feedback in #57688 (#57961) * Wrap error messages in sprintf * Use await rather than then * Add variables for API URLs * Update packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js Co-authored-by: Jeff Ong --------- Co-authored-by: Jeff Ong --- .../font-library-modal/context.js | 17 +++-- .../font-library-modal/resolvers.js | 72 +++++++++---------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index c653d0a8b03626..4c305ef8e7e60e 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -9,7 +9,7 @@ import { useEntityRecords, store as coreStore, } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -265,8 +265,11 @@ function FontLibraryProvider( { children } ) { alreadyInstalledFontFaces.length === 0 ) { throw new Error( - __( 'No font faces were installed. ' ) + + sprintf( + /* translators: %s: Specific error message returned from server. */ + __( 'No font faces were installed. %s' ), detailedErrorMessage + ) ); } @@ -289,9 +292,13 @@ function FontLibraryProvider( { children } ) { if ( unsucessfullyInstalledFontFaces.length > 0 ) { throw new Error( - __( - 'Some font faces were installed. There were some errors. ' - ) + detailedErrorMessage + sprintf( + /* translators: %s: Specific error message returned from server. */ + __( + 'Some font faces were installed. There were some errors. %s' + ), + detailedErrorMessage + ) ); } } finally { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index baaa6985995538..6dcfe85118be2a 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -7,76 +7,76 @@ */ import apiFetch from '@wordpress/api-fetch'; +const FONT_FAMILIES_URL = '/wp/v2/font-families'; +const FONT_COLLECTIONS_URL = '/wp/v2/font-collections'; + export async function fetchInstallFontFamily( data ) { const config = { - path: '/wp/v2/font-families', + path: FONT_FAMILIES_URL, method: 'POST', body: data, }; - return apiFetch( config ).then( ( response ) => { - return { - id: response.id, - ...response.font_face_settings, - fontFace: [], - }; - } ); + const response = await apiFetch( config ); + return { + id: response.id, + ...response.font_family_settings, + fontFace: [], + }; } export async function fetchInstallFontFace( fontFamilyId, data ) { const config = { - path: `/wp/v2/font-families/${ fontFamilyId }/font-faces`, + path: `${ FONT_FAMILIES_URL }/${ fontFamilyId }/font-faces`, method: 'POST', body: data, }; - return apiFetch( config ).then( ( response ) => { - return { - id: response.id, - ...response.font_face_settings, - }; - } ); + const response = await apiFetch( config ); + return { + id: response.id, + ...response.font_face_settings, + }; } export async function fetchGetFontFamilyBySlug( slug ) { const config = { - path: `/wp/v2/font-families?slug=${ slug }&_embed=true`, + path: `${ FONT_FAMILIES_URL }?slug=${ slug }&_embed=true`, method: 'GET', }; - return apiFetch( config ).then( ( response ) => { - if ( ! response || response.length === 0 ) { - return null; - } - const fontFamilyPost = response[ 0 ]; - return { - id: fontFamilyPost.id, - ...fontFamilyPost.font_family_settings, - fontFace: - fontFamilyPost?._embedded?.font_faces.map( - ( face ) => face.font_face_settings - ) || [], - }; - } ); + const response = await apiFetch( config ); + if ( ! response || response.length === 0 ) { + return null; + } + const fontFamilyPost = response[ 0 ]; + return { + id: fontFamilyPost.id, + ...fontFamilyPost.font_family_settings, + fontFace: + fontFamilyPost?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || [], + }; } export async function fetchUninstallFontFamily( fontFamilyId ) { const config = { - path: `/wp/v2/font-families/${ fontFamilyId }?force=true`, + path: `${ FONT_FAMILIES_URL }/${ fontFamilyId }?force=true`, method: 'DELETE', }; - return apiFetch( config ); + return await apiFetch( config ); } export async function fetchFontCollections() { const config = { - path: '/wp/v2/font-collections', + path: FONT_COLLECTIONS_URL, method: 'GET', }; - return apiFetch( config ); + return await apiFetch( config ); } export async function fetchFontCollection( id ) { const config = { - path: `/wp/v2/font-collections/${ id }`, + path: `${ FONT_COLLECTIONS_URL }/${ id }`, method: 'GET', }; - return apiFetch( config ); + return await apiFetch( config ); } From 32689e176c2d2df55803a1fa06903ac148e8df7d Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Thu, 18 Jan 2024 20:02:52 -0600 Subject: [PATCH 13/16] Font Library REST API endpoints: address initial feedback from feature branch (#57946) --- ...rest-autosave-font-families-controller.php | 25 -- ...lass-wp-rest-autosave-fonts-controller.php | 25 ++ .../class-wp-rest-font-faces-controller.php | 242 +++++++++------ ...class-wp-rest-font-families-controller.php | 289 +++++++++--------- .../fonts/font-library/font-library.php | 44 +-- lib/load.php | 2 +- .../wpRestFontFacesController.php | 74 ++++- .../wpRestFontFamiliesController.php | 49 ++- 8 files changed, 446 insertions(+), 304 deletions(-) delete mode 100644 lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php create mode 100644 lib/experimental/fonts/font-library/class-wp-rest-autosave-fonts-controller.php diff --git a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php deleted file mode 100644 index 0e31bd4004b40f..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php +++ /dev/null @@ -1,25 +0,0 @@ -post_type = $post_type; - - $post_type_obj = get_post_type_object( $post_type ); - $this->rest_base = $post_type_obj->rest_base; - - $parent_post_type = 'wp_font_family'; - $this->parent_post_type = $parent_post_type; - $parent_post_type_obj = get_post_type_object( $parent_post_type ); - $this->parent_base = $parent_post_type_obj->rest_base; - $this->namespace = $parent_post_type_obj->rest_namespace; - } + protected $allow_batch = false; /** * Registers the routes for posts. @@ -55,7 +33,7 @@ public function __construct() { public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, + '/' . $this->rest_base, array( 'args' => array( 'font_family_id' => array( @@ -67,7 +45,7 @@ public function register_routes() { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_font_faces_permissions_check' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( @@ -82,7 +60,7 @@ public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base . '/(?P[\d]+)', + '/' . $this->rest_base . '/(?P[\d]+)', array( 'args' => array( 'font_family_id' => array( @@ -99,8 +77,10 @@ public function register_routes() { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_font_faces_permissions_check' ), - 'args' => array(), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'edit' ) ), + ), ), array( 'methods' => WP_REST_Server::DELETABLE, @@ -124,12 +104,13 @@ public function register_routes() { * * @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_font_faces_permissions_check() { + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class $post_type = get_post_type_object( $this->post_type ); - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + if ( ! current_user_can( $post_type->cap->read ) ) { return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to access font faces.', 'gutenberg' ), @@ -140,6 +121,18 @@ public function get_font_faces_permissions_check() { 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 ) { + return $this->get_items_permissions_check( $request ); + } + /** * Validates settings when creating a font face. * @@ -240,7 +233,7 @@ public function sanitize_font_face_settings( $value ) { * @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_font_family_post( $request['font_family_id'] ); + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); if ( is_wp_error( $font_family ) ) { return $font_family; } @@ -263,14 +256,12 @@ public function get_item( $request ) { } // Check that the font face has a valid parent font family. - $font_family = $this->get_font_family_post( $request['font_family_id'] ); + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); if ( is_wp_error( $font_family ) ) { return $font_family; } - $response = parent::get_item( $request ); - - if ( (int) $font_family->ID !== (int) $response->data['parent'] ) { + if ( (int) $font_family->ID !== (int) $post->post_parent ) { return new WP_Error( 'rest_font_face_parent_id_mismatch', /* translators: %d: A post id. */ @@ -279,7 +270,7 @@ public function get_item( $request ) { ); } - return $response; + return parent::get_item( $request ); } /** @@ -291,19 +282,26 @@ public function get_item( $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. - $existing_font_face = get_posts( + $query = new WP_Query( array( - 'post_type' => $this->post_type, - 'posts_per_page' => 1, - 'title' => WP_Font_Family_Utils::get_font_face_slug( $settings ), + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'title' => WP_Font_Family_Utils::get_font_face_slug( $settings ), + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, ) ); - if ( ! empty( $existing_font_face ) ) { + if ( ! empty( $query->get_posts() ) ) { return new WP_Error( 'rest_duplicate_font_face', __( 'A font face matching those settings already exists.', 'gutenberg' ), @@ -367,9 +365,28 @@ public function create_item( $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"', 'gutenberg' ), $font_family->ID ), + array( 'status' => 404 ) + ); + } + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - // We don't support trashing for revisions. + // We don't support trashing for font faces. if ( ! $force ) { return new WP_Error( 'rest_trash_not_supported', @@ -391,19 +408,46 @@ public function delete_item( $request ) { * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ - public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class - $data = array(); + 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'] = 2; + } - $data['id'] = $item->ID; - $data['theme_json_version'] = 2; - $data['parent'] = $item->post_parent; - $data['font_face_settings'] = $this->get_settings_from_post( $item ); + 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'] : 'edit'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); - $links = $this->prepare_links( $item ); - $response->add_links( $links ); - return $response; + 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 ); } /** @@ -427,6 +471,7 @@ public function get_item_schema() { 'id' => array( 'description' => __( 'Unique identifier for the post.', 'default' ), 'type' => 'integer', + 'context' => array( 'edit', 'embed' ), 'readonly' => true, ), 'theme_json_version' => array( @@ -435,36 +480,39 @@ public function get_item_schema() { 'default' => 2, 'minimum' => 2, 'maximum' => 2, + 'context' => array( 'edit', 'embed' ), ), 'parent' => array( 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), 'type' => 'integer', + 'context' => array( '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.', 'gutenberg' ), 'type' => 'object', + 'context' => array( 'edit', 'embed' ), 'properties' => array( 'fontFamily' => array( - 'description' => 'CSS font-family value.', + 'description' => __( 'CSS font-family value.', 'gutenberg' ), 'type' => 'string', 'default' => '', ), 'fontStyle' => array( - 'description' => 'CSS font-style value.', + 'description' => __( 'CSS font-style value.', 'gutenberg' ), 'type' => 'string', 'default' => 'normal', ), 'fontWeight' => array( - 'description' => 'List of available font weights, separated by a space.', + 'description' => __( 'List of available font weights, separated by a space.', 'gutenberg' ), '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' ), ), 'fontDisplay' => array( - 'description' => 'CSS font-display value.', + 'description' => __( 'CSS font-display value.', 'gutenberg' ), 'type' => 'string', 'default' => 'fallback', 'enum' => array( @@ -476,7 +524,7 @@ public function get_item_schema() { ), ), 'src' => array( - 'description' => 'Paths or URLs to the font files.', + 'description' => __( 'Paths or URLs to the font files.', 'gutenberg' ), // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array. 'anyOf' => array( array( @@ -492,43 +540,43 @@ public function get_item_schema() { 'default' => array(), ), 'fontStretch' => array( - 'description' => 'CSS font-stretch value.', + 'description' => __( 'CSS font-stretch value.', 'gutenberg' ), 'type' => 'string', ), 'ascentOverride' => array( - 'description' => 'CSS ascent-override value.', + 'description' => __( 'CSS ascent-override value.', 'gutenberg' ), 'type' => 'string', ), 'descentOverride' => array( - 'description' => 'CSS descent-override value.', + 'description' => __( 'CSS descent-override value.', 'gutenberg' ), 'type' => 'string', ), 'fontVariant' => array( - 'description' => 'CSS font-variant value.', + 'description' => __( 'CSS font-variant value.', 'gutenberg' ), 'type' => 'string', ), 'fontFeatureSettings' => array( - 'description' => 'CSS font-feature-settings value.', + 'description' => __( 'CSS font-feature-settings value.', 'gutenberg' ), 'type' => 'string', ), 'fontVariationSettings' => array( - 'description' => 'CSS font-variation-settings value.', + 'description' => __( 'CSS font-variation-settings value.', 'gutenberg' ), 'type' => 'string', ), 'lineGapOverride' => array( - 'description' => 'CSS line-gap-override value.', + 'description' => __( 'CSS line-gap-override value.', 'gutenberg' ), 'type' => 'string', ), 'sizeAdjust' => array( - 'description' => 'CSS size-adjust value.', + 'description' => __( 'CSS size-adjust value.', 'gutenberg' ), 'type' => 'string', ), 'unicodeRange' => array( - 'description' => 'CSS unicode-range value.', + 'description' => __( 'CSS unicode-range value.', 'gutenberg' ), 'type' => 'string', ), 'preview' => array( - 'description' => 'URL to a preview image of the font face.', + 'description' => __( 'URL to a preview image of the font face.', 'gutenberg' ), 'type' => 'string', ), ), @@ -551,13 +599,31 @@ public function get_item_schema() { * @return array Collection parameters. */ public function get_collection_params() { - $params = parent::get_collection_params(); - - return array( - 'page' => $params['page'], - 'per_page' => $params['per_page'], - 'search' => $params['search'], - ); + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'edit'; + + // Remove unneeded params. + unset( $query_params['after'] ); + unset( $query_params['modified_after'] ); + unset( $query_params['before'] ); + unset( $query_params['modified_before'] ); + unset( $query_params['search'] ); + unset( $query_params['search_columns'] ); + unset( $query_params['slug'] ); + unset( $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 ); } /** @@ -571,7 +637,7 @@ public function get_create_params() { $properties = $this->get_item_schema()['properties']; return array( 'theme_json_version' => $properties['theme_json_version'], - // Font face settings is stringified JSON, to work with multipart/form-data used + // 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.', 'gutenberg' ), @@ -583,18 +649,6 @@ public function get_create_params() { ); } - /** - * Allow the font face post type to be managed through the REST API. - * - * @since 6.5.0 - * - * @param WP_Post_Type|string $post_type Post type name or object. - * @return bool Whether the post type is allowed in REST. - */ - protected function check_is_post_type_allowed( $post_type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class - return true; - } - /** * Get the parent font family, if the ID is valid. * @@ -603,7 +657,7 @@ protected function check_is_post_type_allowed( $post_type ) { // phpcs:ignore Va * @param int $font_family_id Supplied ID. * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. */ - protected function get_font_family_post( $font_family_id ) { + protected function get_parent_font_family_post( $font_family_id ) { $error = new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent ID.', 'default' ), @@ -617,7 +671,7 @@ protected function get_font_family_post( $font_family_id ) { $font_family_post = get_post( (int) $font_family_id ); if ( empty( $font_family_post ) || empty( $font_family_post->ID ) - || $this->parent_post_type !== $font_family_post->post_type + || 'wp_font_family' !== $font_family_post->post_type ) { return $error; } @@ -637,13 +691,13 @@ protected function prepare_links( $post ) { // Entity meta. $links = array( 'self' => array( - 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base . '/' . $post->ID ), + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), ), 'collection' => array( - 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent . '/' . $this->rest_base ), + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ), ), 'parent' => array( - 'href' => rest_url( $this->namespace . '/' . $this->parent_base . '/' . $post->post_parent ), + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ), ), ); @@ -725,7 +779,7 @@ 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 ) { + if ( __( 'Sorry, you are not allowed to upload this file type.', 'default' ) === $message ) { $status = 400; $code = 'rest_font_upload_invalid_file_type'; } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index a48c9cbe1a7763..887a8a5250cc32 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -18,81 +18,12 @@ */ class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller { /** - * Constructor. + * Whether the controller supports batching. * * @since 6.5.0 + * @var false */ - public function __construct() { - $post_type = 'wp_font_family'; - $this->post_type = $post_type; - - $post_type_obj = get_post_type_object( $post_type ); - $this->rest_base = $post_type_obj->rest_base; - $this->namespace = $post_type_obj->rest_namespace; - } - - /** - * 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_font_families_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_edit_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\d]+)', - array( - 'id' => array( - 'description' => __( 'Unique identifier for the font family.', 'gutenberg' ), - 'type' => 'integer', - 'required' => true, - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_font_families_permissions_check' ), - 'args' => array(), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_create_edit_params(), - ), - 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' ), - ), - ), - ), - ) - ); - } + protected $allow_batch = false; /** * Checks if a given request has access to font families. @@ -102,13 +33,13 @@ public function register_routes() { * @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_font_families_permissions_check() { + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class $post_type = get_post_type_object( $this->post_type ); - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + if ( ! current_user_can( $post_type->cap->read ) ) { return new WP_Error( 'rest_cannot_read', - __( 'Sorry, you are not allowed to access font faces.', 'gutenberg' ), + __( 'Sorry, you are not allowed to access font families.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -116,6 +47,18 @@ public function get_font_families_permissions_check() { 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 ) { + return $this->get_items_permissions_check( $request ); + } + /** * Validates settings when creating or updating a font family. * @@ -213,14 +156,16 @@ public function create_item( $request ) { $settings = $request->get_param( 'font_family_settings' ); // Check that the font family slug is unique. - $existing_font_family = get_posts( + $query = new WP_Query( array( - 'post_type' => $this->post_type, - 'posts_per_page' => 1, - 'name' => $settings['slug'], + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'name' => $settings['slug'], + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, ) ); - if ( ! empty( $existing_font_family ) ) { + if ( ! empty( $query->get_posts() ) ) { return new WP_Error( 'rest_duplicate_font_family', /* translators: %s: Font family slug. */ @@ -241,10 +186,9 @@ public function create_item( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function delete_item( $request ) { - $font_family_id = $request->get_param( 'id' ); - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - // We don't support trashing for revisions. + // We don't support trashing for font families. if ( ! $force ) { return new WP_Error( 'rest_trash_not_supported', @@ -254,17 +198,7 @@ public function delete_item( $request ) { ); } - $deleted = parent::delete_item( $request ); - - if ( is_wp_error( $deleted ) ) { - return $deleted; - } - - foreach ( $this->get_font_face_ids( $font_family_id ) as $font_face_id ) { - wp_delete_post( $font_face_id, true ); - } - - return $deleted; + return parent::delete_item( $request ); } /** @@ -276,19 +210,47 @@ public function delete_item( $request ) { * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ - public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class - $data = array(); + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); - $data['id'] = $item->ID; - $data['theme_json_version'] = 2; - $data['font_faces'] = $this->get_font_face_ids( $item->ID ); - $data['font_family_settings'] = $this->get_settings_from_post( $item ); + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $item->ID; + } + + if ( rest_is_field_included( 'theme_json_version', $fields ) ) { + $data['theme_json_version'] = 2; + } + + 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'] : 'edit'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); - $links = $this->prepare_links( $item ); - $response->add_links( $links ); - return $response; + 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 ); } /** @@ -312,6 +274,7 @@ public function get_item_schema() { 'id' => array( 'description' => __( 'Unique identifier for the post.', 'default' ), 'type' => 'integer', + 'context' => array( 'edit' ), 'readonly' => true, ), 'theme_json_version' => array( @@ -320,10 +283,12 @@ public function get_item_schema() { 'default' => 2, 'minimum' => 2, 'maximum' => 2, + 'context' => array( 'edit' ), ), 'font_faces' => array( 'description' => __( 'The IDs of the child font faces in the font family.', 'gutenberg' ), 'type' => 'array', + 'context' => array( 'edit' ), 'items' => array( 'type' => 'integer', ), @@ -333,6 +298,7 @@ public function get_item_schema() { 'font_family_settings' => array( 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ), 'type' => 'object', + 'context' => array( 'edit' ), 'properties' => array( 'name' => array( 'description' => 'Name of the font family preset, translatable.', @@ -370,58 +336,72 @@ public function get_item_schema() { * @return array Collection parameters. */ public function get_collection_params() { - $params = parent::get_collection_params(); + $query_params = parent::get_collection_params(); - return array( - 'page' => $params['page'], - 'per_page' => $params['per_page'], - 'search' => $params['search'], - 'slug' => $params['slug'], - ); + $query_params['context']['default'] = 'edit'; + + // Remove unneeded params. + unset( $query_params['after'] ); + unset( $query_params['modified_after'] ); + unset( $query_params['before'] ); + unset( $query_params['modified_before'] ); + unset( $query_params['search'] ); + unset( $query_params['search_columns'] ); + unset( $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 ); } /** - * Checks if a given request has access to read font families. + * Retrieves the query params for the font family collection, defaulting to the 'edit' context. * * @since 6.5.0 * - * @return true|WP_Error True if the request has read access, otherwise a WP_Error object. + * @param array $args Optional. Additional arguments for context parameter. Default empty array. + * @return array Context parameter details. */ - public function get_font_family_permissions_check() { - $post_type = get_post_type_object( $this->post_type ); - - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { - return new WP_Error( - 'rest_cannot_read', - __( 'Sorry, you are not allowed to access font families.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); + public function get_context_param( $args = array() ) { + if ( isset( $args['default'] ) ) { + $args['default'] = 'edit'; } - - return true; + return parent::get_context_param( $args ); } /** - * Get the params used when creating or updating a font family. + * 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_create_edit_params() { - $properties = $this->get_item_schema()['properties']; - return array( - 'theme_json_version' => $properties['theme_json_version'], - // 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.', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - 'validate_callback' => array( $this, 'validate_font_family_settings' ), - 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ), - ), - ); + 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.', 'gutenberg' ), + '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 ); } /** @@ -434,19 +414,24 @@ public function get_create_edit_params() { * . */ protected function get_font_face_ids( $font_family_id ) { - $font_face_ids = get_posts( + $query = new WP_Query( array( - 'fields' => 'ids', - 'post_parent' => $font_family_id, - 'post_type' => 'wp_font_face', - 'posts_per_page' => 999, + '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 $font_face_ids; + + return $query->get_posts(); } /** - * Prepares links for the request. + * Prepares font family links for the request. * * @since 6.5.0 * @@ -464,6 +449,12 @@ protected function prepare_links( $post ) { ); } + /** + * 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(); @@ -528,10 +519,10 @@ protected function get_settings_from_post( $post ) { // Default to empty strings if the settings are missing. return array( - 'name' => $post->post_title ?? '', - 'slug' => $post->post_name ?? '', - 'fontFamily' => $settings_json['fontFamily'] ?? '', - 'preview' => $settings_json['preview'] ?? '', + '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'] : '', ); } } diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index eb76cbab191280..a156089a071c55 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -27,8 +27,8 @@ function gutenberg_init_font_library_routes() { 'singular_name' => __( 'Font Family', 'gutenberg' ), ), 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'show_in_rest' => true, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, 'capabilities' => array( 'read' => 'edit_theme_options', 'read_post' => 'edit_theme_options', @@ -45,26 +45,25 @@ function gutenberg_init_font_library_routes() { 'delete_published_posts' => 'edit_theme_options', ), 'map_meta_cap' => false, + 'query_var' => false, + 'show_in_rest' => true, 'rest_base' => 'font-families', 'rest_controller_class' => 'WP_REST_Font_Families_Controller', - 'autosave_rest_controller_class' => 'WP_REST_Autosave_Font_Families_Controller', - 'query_var' => false, + 'autosave_rest_controller_class' => 'WP_REST_Autosave_Fonts_Controller', ); register_post_type( 'wp_font_family', $args ); register_post_type( 'wp_font_face', array( - 'labels' => array( + 'labels' => array( 'name' => __( 'Font Faces', 'gutenberg' ), 'singular_name' => __( 'Font Face', 'gutenberg' ), ), - 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'hierarchical' => false, - 'show_in_rest' => false, - 'rest_base' => 'font-faces', - 'capabilities' => array( + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'capabilities' => array( 'read' => 'edit_theme_options', 'read_post' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', @@ -79,17 +78,18 @@ function gutenberg_init_font_library_routes() { 'delete_others_posts' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', ), - 'map_meta_cap' => false, - 'query_var' => false, + 'map_meta_cap' => false, + 'query_var' => false, + 'show_in_rest' => true, + 'rest_base' => 'font-families/(?P[\d]+)/font-faces', + 'rest_controller_class' => 'WP_REST_Font_Faces_Controller', + 'autosave_rest_controller_class' => 'WP_REST_Autosave_Fonts_Controller', ) ); // @core-merge: This code will go into Core's `create_initial_rest_routes()`. $font_collections_controller = new WP_REST_Font_Collections_Controller(); $font_collections_controller->register_routes(); - - $font_faces_controller = new WP_REST_Font_Faces_Controller(); - $font_faces_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); @@ -190,7 +190,7 @@ function wp_get_font_dir( $defaults = array() ) { // @core-merge: Filters should go in `src/wp-includes/default-filters.php`, // functions in a general file for font library. -if ( ! function_exists( '_wp_delete_font_family' ) ) { +if ( ! function_exists( '_wp_after_delete_font_family' ) ) { /** * Deletes child font faces when a font family is deleted. * @@ -201,7 +201,7 @@ function wp_get_font_dir( $defaults = array() ) { * @param WP_Post $post Post object. * @return void */ - function _wp_delete_font_family( $post_id, $post ) { + function _wp_after_delete_font_family( $post_id, $post ) { if ( 'wp_font_family' !== $post->post_type ) { return; } @@ -217,10 +217,10 @@ function _wp_delete_font_family( $post_id, $post ) { wp_delete_post( $font_face->ID, true ); } } - add_action( 'deleted_post', '_wp_delete_font_family', 10, 2 ); + add_action( 'deleted_post', '_wp_after_delete_font_family', 10, 2 ); } -if ( ! function_exists( '_wp_delete_font_face' ) ) { +if ( ! function_exists( '_wp_before_delete_font_face' ) ) { /** * Deletes associated font files when a font face is deleted. * @@ -231,7 +231,7 @@ function _wp_delete_font_family( $post_id, $post ) { * @param WP_Post $post Post object. * @return void */ - function _wp_delete_font_face( $post_id, $post ) { + function _wp_before_delete_font_face( $post_id, $post ) { if ( 'wp_font_face' !== $post->post_type ) { return; } @@ -242,5 +242,5 @@ function _wp_delete_font_face( $post_id, $post ) { wp_delete_file( wp_get_font_dir()['path'] . '/' . $font_file ); } } - add_action( 'before_delete_post', '_wp_delete_font_face', 10, 2 ); + add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 ); } diff --git a/lib/load.php b/lib/load.php index b569c52d14e253..81daeef25933f3 100644 --- a/lib/load.php +++ b/lib/load.php @@ -157,7 +157,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php'; - require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php'; + require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-autosave-fonts-controller.php'; require __DIR__ . '/experimental/fonts/font-library/font-library.php'; } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index a53289735c4297..55db0ce18392d1 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -112,10 +112,24 @@ public function test_register_routes() { } /** - * @doesNotPerformAssertions + * @covers WP_REST_Font_Faces_Controller::get_context_param */ public function test_context_param() { - // Controller does not use get_context_param(). + // Collection. + $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->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); + $this->assertSame( 'edit', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/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->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); + $this->assertSame( 'edit', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } /** @@ -130,9 +144,9 @@ public function test_get_items() { $this->assertSame( 200, $response->get_status() ); $this->assertCount( 2, $data ); $this->assertArrayHasKey( '_links', $data[0] ); - $this->check_font_face_data( $data[0], self::$font_face_id1, $data[0]['_links'] ); + $this->check_font_face_data( $data[0], self::$font_face_id2, $data[0]['_links'] ); $this->assertArrayHasKey( '_links', $data[1] ); - $this->check_font_face_data( $data[1], self::$font_face_id2, $data[1]['_links'] ); + $this->check_font_face_data( $data[1], self::$font_face_id1, $data[1]['_links'] ); } /** @@ -465,6 +479,21 @@ public function test_create_item_with_all_properties() { wp_delete_post( $data['id'], true ); } + /** + * @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 */ @@ -675,10 +704,10 @@ public function test_update_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['force'] = true; - $response = rest_get_server()->dispatch( $request ); + $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() ); $this->assertNull( get_post( $font_face_id ) ); @@ -710,11 +739,38 @@ public function test_delete_item_no_trash() { */ 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 = 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 ); + + $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 */ diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index e0fe5e0c8cb93e..4fffa879ad56ed 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -127,12 +127,27 @@ public function test_register_routes() { } /** - * @doesNotPerformAssertions + * @covers WP_REST_Font_Families_Controller::get_context_param */ public function test_context_param() { - // Controller does not use get_context_param(). + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); + $this->assertSame( 'edit', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families/' . self::$font_family_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); + $this->assertSame( 'edit', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } + /** * @covers WP_REST_Font_Faces_Controller::get_items */ @@ -145,9 +160,9 @@ public function test_get_items() { $this->assertSame( 200, $response->get_status() ); $this->assertCount( 2, $data ); $this->assertArrayHasKey( '_links', $data[0] ); - $this->check_font_family_data( $data[0], self::$font_family_id1, $data[0]['_links'] ); + $this->check_font_family_data( $data[0], self::$font_family_id2, $data[0]['_links'] ); $this->assertArrayHasKey( '_links', $data[1] ); - $this->check_font_family_data( $data[1], self::$font_family_id2, $data[1]['_links'] ); + $this->check_font_family_data( $data[1], self::$font_family_id1, $data[1]['_links'] ); } /** @@ -195,6 +210,32 @@ public function test_get_item() { $this->check_font_family_data( $data, self::$font_family_id1, $response->get_links() ); } + /** + * @covers WP_REST_Font_Faces_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() ); + $this->assertArrayHasKey( '_embedded', $data ); + $this->assertArrayHasKey( 'font_faces', $data['_embedded'] ); + $this->assertCount( 2, $data['_embedded']['font_faces'] ); + + foreach ( $data['_embedded']['font_faces'] as $font_face ) { + $this->assertArrayHasKey( 'id', $font_face ); + + $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 ); + } + } + /** * @covers WP_REST_Font_Families_Controller::get_item */ From 2aa9c642262ad1d82c202e3dc6d12e207d30bd43 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 19 Jan 2024 11:14:42 -0300 Subject: [PATCH 14/16] Font Library: font collection refactor to use the new schema (#57884) * google fonts collection data provisional url * rename controller methods * fix get_items parameters * fix endpoint return * rafactor font collection class * fix tests for the refactored class * refactor font collections rest controller * update font collection tests * update the frontend to use the new endpoint data schema * format php * adding linter line ignore rul * replacing throwing an exception by calling doing_it_wrong * add translation marks Co-authored-by: Jeff Ong * user ternary operator * correct translation formatting and comments * renaming function * renaming tests * improve url matching Co-authored-by: Grant Kinney * return error without rest_ensure_response * fix contradictory if condition --------- Co-authored-by: Jeff Ong Co-authored-by: Grant Kinney --- .../font-library/class-wp-font-collection.php | 183 ++++++++++++++---- .../font-library/class-wp-font-library.php | 12 +- ...ss-wp-rest-font-collections-controller.php | 66 ++++--- .../fonts/font-library/font-library.php | 3 +- .../font-library-modal/context.js | 12 +- .../font-library-modal/font-collection.js | 28 +-- .../global-styles/font-library-modal/index.js | 8 +- .../wpFontCollection/__construct.php | 51 +++-- .../wpFontCollection/getConfig.php | 14 +- .../{getConfigAndData.php => getContent.php} | 70 +++---- .../wpFontCollection/isConfigValid.php | 103 ++++++++++ .../font-library/wpFontFamily/__construct.php | 6 +- .../wpFontLibrary/registerFontCollection.php | 18 +- .../wpRestFontCollectionsController.php | 163 ++++++++++++++++ .../wpRestFontCollectionsController/base.php | 42 ---- .../getFontCollection.php | 126 ------------ .../getFontCollections.php | 45 ----- .../registerRoutes.php | 24 --- 18 files changed, 563 insertions(+), 411 deletions(-) rename phpunit/tests/fonts/font-library/wpFontCollection/{getConfigAndData.php => getContent.php} (52%) create mode 100644 phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php create mode 100644 phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php index 6189da5fa984b1..1ff96b1343b453 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-collection.php +++ b/lib/experimental/fonts/font-library/class-wp-font-collection.php @@ -21,41 +21,128 @@ class WP_Font_Collection { /** - * Font collection configuration. + * The unique slug for the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $slug; + + /** + * The name of the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $name; + + /** + * Description of the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $description; + + /** + * Source of the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $src; + + /** + * Array of font families in the collection. * * @since 6.5.0 * * @var array */ - private $config; + private $font_families; + + /** + * Categories associated with the font collection. + * + * @since 6.5.0 + * + * @var array + */ + private $categories; + /** * WP_Font_Collection constructor. * * @since 6.5.0 * - * @param array $config Font collection config options. - * See {@see wp_register_font_collection()} for the supported fields. - * @throws Exception If the required parameters are missing. + * @param array $config Font collection config options. { + * @type string $slug The font collection's unique slug. + * @type string $name The font collection's name. + * @type string $description The font collection's description. + * @type string $src The font collection's source. + * @type array $font_families An array of font families in the font collection. + * @type array $categories The font collection's categories. + * } */ public function __construct( $config ) { - if ( empty( $config ) || ! is_array( $config ) ) { - throw new Exception( 'Font Collection config options is required as a non-empty array.' ); - } + $this->is_config_valid( $config ); + + $this->slug = isset( $config['slug'] ) ? $config['slug'] : ''; + $this->name = isset( $config['name'] ) ? $config['name'] : ''; + $this->description = isset( $config['description'] ) ? $config['description'] : ''; + $this->src = isset( $config['src'] ) ? $config['src'] : ''; + $this->font_families = isset( $config['font_families'] ) ? $config['font_families'] : array(); + $this->categories = isset( $config['categories'] ) ? $config['categories'] : array(); + } - if ( empty( $config['slug'] ) || ! is_string( $config['slug'] ) ) { - throw new Exception( 'Font Collection config slug is required as a non-empty string.' ); + /** + * Checks if the font collection config is valid. + * + * @since 6.5.0 + * + * @param array $config Font collection config options. { + * @type string $slug The font collection's unique slug. + * @type string $name The font collection's name. + * @type string $description The font collection's description. + * @type string $src The font collection's source. + * @type array $font_families An array of font families in the font collection. + * @type array $categories The font collection's categories. + * } + * @return bool True if the font collection config is valid and false otherwise. + */ + public static function is_config_valid( $config ) { + if ( empty( $config ) || ! is_array( $config ) ) { + _doing_it_wrong( __METHOD__, __( 'Font Collection config options are required as a non-empty array.', 'gutenberg' ), '6.5.0' ); + return false; } - if ( empty( $config['name'] ) || ! is_string( $config['name'] ) ) { - throw new Exception( 'Font Collection config name is required as a non-empty string.' ); + $required_keys = array( 'slug', 'name' ); + foreach ( $required_keys as $key ) { + if ( empty( $config[ $key ] ) ) { + _doing_it_wrong( + __METHOD__, + // translators: %s: Font collection config key. + sprintf( __( 'Font Collection config %s is required as a non-empty string.', 'gutenberg' ), $key ), + '6.5.0' + ); + return false; + } } - if ( ( empty( $config['src'] ) || ! is_string( $config['src'] ) ) && ( empty( $config['data'] ) ) ) { - throw new Exception( 'Font Collection config "src" option OR "data" option is required.' ); + if ( + ( empty( $config['src'] ) && empty( $config['font_families'] ) ) || + ( ! empty( $config['src'] ) && ! empty( $config['font_families'] ) ) + ) { + _doing_it_wrong( __METHOD__, __( 'Font Collection config "src" option OR "font_families" option are required.', 'gutenberg' ), '6.5.0' ); + return false; } - $this->config = $config; + return true; } /** @@ -73,56 +160,59 @@ public function __construct( $config ) { */ public function get_config() { return array( - 'slug' => $this->config['slug'], - 'name' => $this->config['name'], - 'description' => $this->config['description'] ?? '', + 'slug' => $this->slug, + 'name' => $this->name, + 'description' => $this->description, ); } /** - * Gets the font collection config and data. + * Gets the font collection content. * - * This function returns an array containing the font collection's unique ID, - * name, and its data as a PHP array. + * Load the font collection data from the src if it is not already loaded. * * @since 6.5.0 * - * @return array { - * An array of font collection config and data. + * @return array|WP_Error { + * An array of font collection contents. * - * @type string $slug The font collection's unique ID. - * @type string $name The font collection's name. - * @type string $description The font collection's description. - * @type array $data The font collection's data as a PHP array. + * @type array $font_families The font collection's font families. + * @type string $categories The font collection's categories. * } + * + * A WP_Error object if there was an error loading the font collection data. */ - public function get_config_and_data() { - $config_and_data = $this->get_config(); - $config_and_data['data'] = $this->load_data(); - return $config_and_data; + public function get_content() { + // If the font families are not loaded, and the src is not empty, load the data from the src. + if ( empty( $this->font_families ) && ! empty( $this->src ) ) { + $data = $this->load_contents_from_src(); + if ( is_wp_error( $data ) ) { + return $data; + } + } + + return array( + 'font_families' => $this->font_families, + 'categories' => $this->categories, + ); } /** - * Loads the font collection data. + * Loads the font collection data from the src. * * @since 6.5.0 * * @return array|WP_Error An array containing the list of font families in font-collection.json format on success, * else an instance of WP_Error on failure. */ - public function load_data() { - - if ( ! empty( $this->config['data'] ) ) { - return $this->config['data']; - } - + private function load_contents_from_src() { // If the src is a URL, fetch the data from the URL. - if ( str_contains( $this->config['src'], 'http' ) && str_contains( $this->config['src'], '://' ) ) { - if ( ! wp_http_validate_url( $this->config['src'] ) ) { + if ( preg_match( '#^https?://#', $this->src ) ) { + if ( ! wp_http_validate_url( $this->src ) ) { return new WP_Error( 'font_collection_read_error', __( 'Invalid URL for Font Collection data.', 'gutenberg' ) ); } - $response = wp_remote_get( $this->config['src'] ); + $response = wp_remote_get( $this->src ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return new WP_Error( 'font_collection_read_error', __( 'Error fetching the Font Collection data from a URL.', 'gutenberg' ) ); } @@ -133,15 +223,22 @@ public function load_data() { } // If the src is a file path, read the data from the file. } else { - if ( ! file_exists( $this->config['src'] ) ) { + if ( ! file_exists( $this->src ) ) { return new WP_Error( 'font_collection_read_error', __( 'Font Collection data JSON file does not exist.', 'gutenberg' ) ); } - $data = wp_json_file_decode( $this->config['src'], array( 'associative' => true ) ); + $data = wp_json_file_decode( $this->src, array( 'associative' => true ) ); if ( empty( $data ) ) { return new WP_Error( 'font_collection_read_error', __( 'Error reading the Font Collection data JSON file contents.', 'gutenberg' ) ); } } + if ( empty( $data['font_families'] ) ) { + return new WP_Error( 'font_collection_contents_error', __( 'Font Collection data JSON file does not contain font families.', 'gutenberg' ) ); + } + + $this->font_families = $data['font_families']; + $this->categories = $data['categories'] ?? array(); + return $data; } } diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php index fd36f6ba073c4f..51a84b957ea117 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/experimental/fonts/font-library/class-wp-font-library.php @@ -62,11 +62,17 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio * @return WP_Font_Collection|WP_Error A font collection is it was registered successfully and a WP_Error otherwise. */ public static function register_font_collection( $config ) { + if ( ! WP_Font_Collection::is_config_valid( $config ) ) { + $error_message = __( 'Font collection config is invalid.', 'gutenberg' ); + return new WP_Error( 'font_collection_registration_error', $error_message ); + } + $new_collection = new WP_Font_Collection( $config ); - if ( self::is_collection_registered( $config['slug'] ) ) { + + if ( self::is_collection_registered( $new_collection->get_config()['slug'] ) ) { $error_message = sprintf( /* translators: %s: Font collection slug. */ - __( 'Font collection with slug: "%s" is already registered.', 'default' ), + __( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ), $config['slug'] ); _doing_it_wrong( @@ -76,7 +82,7 @@ public static function register_font_collection( $config ) { ); return new WP_Error( 'font_collection_registration_error', $error_message ); } - self::$collections[ $config['slug'] ] = $new_collection; + self::$collections[ $new_collection->get_config()['slug'] ] = $new_collection; return $new_collection; } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php index c7595a56413b9b..51fd14fffaa953 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php @@ -42,8 +42,8 @@ public function register_routes() { array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collections' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); @@ -54,13 +54,29 @@ public function register_routes() { array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collection' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); } + /** + * 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $collections = array(); + foreach ( WP_Font_Library::get_font_collections() as $collection ) { + $collections[] = $collection->get_config(); + } + + return rest_ensure_response( $collections, 200 ); + } + /** * Gets a font collection. * @@ -69,54 +85,42 @@ public function register_routes() { * @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_font_collection( $request ) { + public function get_item( $request ) { $slug = $request->get_param( 'slug' ); $collection = WP_Font_Library::get_font_collection( $slug ); + // If the collection doesn't exist returns a 404. if ( is_wp_error( $collection ) ) { $collection->add_data( array( 'status' => 404 ) ); return $collection; } - $config_and_data = $collection->get_config_and_data(); - $collection_data = $config_and_data['data']; - // If there was an error getting the collection data, return the error. - if ( is_wp_error( $collection_data ) ) { - $collection_data->add_data( array( 'status' => 500 ) ); - return $collection_data; - } - - return new WP_REST_Response( $config_and_data ); - } + $config = $collection->get_config(); + $contents = $collection->get_content(); - /** - * 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_font_collections() { - $collections = array(); - foreach ( WP_Font_Library::get_font_collections() as $collection ) { - $collections[] = $collection->get_config_and_data(); + // If there was an error getting the collection data, return the error. + if ( is_wp_error( $contents ) ) { + $contents->add_data( array( 'status' => 500 ) ); + return $contents; } - return new WP_REST_Response( $collections, 200 ); + $collection_data = array_merge( $config, $contents ); + return rest_ensure_response( $collection_data ); } /** - * Checks whether the user has permissions to update the Font Library. + * 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 update_font_library_permissions_check() { + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( - 'rest_cannot_update_font_library', - __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ), + 'rest_cannot_read', + __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), ) diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index a156089a071c55..e6c5f92da58a0a 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -134,7 +134,8 @@ function wp_unregister_font_collection( $collection_id ) { 'slug' => 'default-font-collection', 'name' => 'Google Fonts', 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), - 'src' => 'https://s.w.org/images/fonts/16.7/collections/google-fonts-with-preview.json', + // TODO: This URL needs to be updated to the wporg hosted one prior to the Gutenberg 17.6 release. + 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/releases/gutenberg-17.6/google-fonts.json', ); wp_register_font_collection( $default_font_collection ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 4c305ef8e7e60e..bc6efaa619e8c1 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -412,16 +412,16 @@ function FontLibraryProvider( { children } ) { const response = await fetchFontCollections(); setFontCollections( response ); }; - const getFontCollection = async ( id ) => { + const getFontCollection = async ( slug ) => { try { const hasData = !! collections.find( - ( collection ) => collection.id === id - )?.data; + ( collection ) => collection.slug === slug + )?.font_families; if ( hasData ) return; - const response = await fetchFontCollection( id ); + const response = await fetchFontCollection( slug ); const updatedCollections = collections.map( ( collection ) => - collection.id === id - ? { ...collection, data: { ...response?.data } } + collection.slug === slug + ? { ...collection, ...response } : collection ); setFontCollections( updatedCollections ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 5b6eeb2481e7a4..793ca9c753f337 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -36,8 +36,8 @@ const DEFAULT_CATEGORY = { id: 'all', name: __( 'All' ), }; -function FontCollection( { id } ) { - const requiresPermission = id === 'default-font-collection'; +function FontCollection( { slug } ) { + const requiresPermission = slug === 'default-font-collection'; const getGoogleFontsPermissionFromStorage = () => { return ( @@ -57,7 +57,7 @@ function FontCollection( { id } ) { const { collections, getFontCollection, installFont } = useContext( FontLibraryContext ); const selectedCollection = collections.find( - ( collection ) => collection.id === id + ( collection ) => collection.slug === slug ); useEffect( () => { @@ -69,12 +69,12 @@ function FontCollection( { id } ) { handleStorage(); window.addEventListener( 'storage', handleStorage ); return () => window.removeEventListener( 'storage', handleStorage ); - }, [ id, requiresPermission ] ); + }, [ slug, requiresPermission ] ); useEffect( () => { const fetchFontCollection = async () => { try { - await getFontCollection( id ); + await getFontCollection( slug ); resetFilters(); } catch ( e ) { setNotice( { @@ -85,12 +85,12 @@ function FontCollection( { id } ) { } }; fetchFontCollection(); - }, [ id, getFontCollection ] ); + }, [ slug, getFontCollection ] ); useEffect( () => { setSelectedFont( null ); setNotice( null ); - }, [ id ] ); + }, [ slug ] ); useEffect( () => { // If the selected fonts change, reset the selected fonts to install @@ -108,10 +108,10 @@ function FontCollection( { id } ) { }, [ notice ] ); const collectionFonts = useMemo( - () => selectedCollection?.data?.fontFamilies ?? [], + () => selectedCollection?.font_families ?? [], [ selectedCollection ] ); - const collectionCategories = selectedCollection?.data?.categories ?? []; + const collectionCategories = selectedCollection?.categories ?? []; const categories = [ DEFAULT_CATEGORY, ...collectionCategories ]; @@ -277,11 +277,11 @@ function FontCollection( { id } ) { { ! renderConfirmDialog && - ! selectedCollection?.data?.fontFamilies && + ! selectedCollection?.font_families && ! notice && } { ! renderConfirmDialog && - !! selectedCollection?.data?.fontFamilies?.length && + !! selectedCollection?.font_families?.length && ! fonts.length && ( { __( @@ -302,10 +302,10 @@ function FontCollection( { id } ) { { fonts.map( ( font ) => ( { - setSelectedFont( font ); + setSelectedFont( font.font_family_settings ); } } /> ) ) } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index 65a284560687cc..a68c42ec010413 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -31,10 +31,10 @@ const DEFAULT_TABS = [ ]; const tabsFromCollections = ( collections ) => - collections.map( ( { id, name } ) => ( { - id, + collections.map( ( { slug, name } ) => ( { + id: slug, title: - collections.length === 1 && id === 'default-font-collection' + collections.length === 1 && slug === 'default-font-collection' ? __( 'Install Fonts' ) : name, } ) ); @@ -76,7 +76,7 @@ function FontLibraryModal( { contents = ; break; default: - contents = ; + contents = ; } return ( setAccessible( true ); + $slug = new ReflectionProperty( WP_Font_Collection::class, 'slug' ); + $slug->setAccessible( true ); - $config = array( + $name = new ReflectionProperty( WP_Font_Collection::class, 'name' ); + $name->setAccessible( true ); + + $description = new ReflectionProperty( WP_Font_Collection::class, 'description' ); + $description->setAccessible( true ); + + $src = new ReflectionProperty( WP_Font_Collection::class, 'src' ); + $src->setAccessible( true ); + + $config = array( 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'src' => 'my-collection-data.json', ); - $font_collection = new WP_Font_Collection( $config ); + $collection = new WP_Font_Collection( $config ); - $actual = $property->getValue( $font_collection ); - $property->setAccessible( false ); + $actual_slug = $slug->getValue( $collection ); + $this->assertSame( 'my-collection', $actual_slug, 'Provided slug and initialized slug should match.' ); + $slug->setAccessible( false ); - $this->assertSame( $config, $actual ); + $actual_name = $name->getValue( $collection ); + $this->assertSame( 'My Collection', $actual_name, 'Provided name and initialized name should match.' ); + $name->setAccessible( false ); + + $actual_description = $description->getValue( $collection ); + $this->assertSame( 'My collection description', $actual_description, 'Provided description and initialized description should match.' ); + $description->setAccessible( false ); + + $actual_src = $src->getValue( $collection ); + $this->assertSame( 'my-collection-data.json', $actual_src, 'Provided src and initialized src should match.' ); + $src->setAccessible( false ); } /** - * @dataProvider data_should_throw_exception + * @dataProvider data_should_do_ti_wrong * * @param mixed $config Config of the font collection. - * @param string $expected_exception_message Expected exception message. */ - public function test_should_throw_exception( $config, $expected_exception_message ) { - $this->expectException( 'Exception' ); - $this->expectExceptionMessage( $expected_exception_message ); + public function test_should_do_ti_wrong( $config ) { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); new WP_Font_Collection( $config ); } @@ -47,7 +65,7 @@ public function test_should_throw_exception( $config, $expected_exception_messag * * @return array */ - public function data_should_throw_exception() { + public function data_should_do_ti_wrong() { return array( 'no id' => array( array( @@ -55,27 +73,22 @@ public function data_should_throw_exception() { 'description' => 'My collection description', 'src' => 'my-collection-data.json', ), - 'Font Collection config slug is required as a non-empty string.', ), 'no config' => array( '', - 'Font Collection config options is required as a non-empty array.', ), 'empty array' => array( array(), - 'Font Collection config options is required as a non-empty array.', ), 'boolean instead of config array' => array( false, - 'Font Collection config options is required as a non-empty array.', ), 'null instead of config array' => array( null, - 'Font Collection config options is required as a non-empty array.', ), 'missing src' => array( @@ -84,9 +97,7 @@ public function data_should_throw_exception() { 'name' => 'My Collection', 'description' => 'My collection description', ), - 'Font Collection config "src" option OR "data" option is required.', ), - ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php b/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php index 5f1f082297d418..393de7d22614da 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php @@ -32,7 +32,7 @@ public function data_should_get_config() { file_put_contents( $mock_file, '{"this is mock data":true}' ); return array( - 'with a file' => array( + 'with a file' => array( 'config' => array( 'slug' => 'my-collection', 'name' => 'My Collection', @@ -45,7 +45,7 @@ public function data_should_get_config() { 'description' => 'My collection description', ), ), - 'with a url' => array( + 'with a url' => array( 'config' => array( 'slug' => 'my-collection-with-url', 'name' => 'My Collection with URL', @@ -58,12 +58,12 @@ public function data_should_get_config() { 'description' => 'My collection description', ), ), - 'with data' => array( + 'with font_families' => array( 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'data' => array( 'this is mock data' => true ), + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( array() ), ), 'expected_data' => array( 'slug' => 'my-collection', diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php b/phpunit/tests/fonts/font-library/wpFontCollection/getContent.php similarity index 52% rename from phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php rename to phpunit/tests/fonts/font-library/wpFontCollection/getContent.php index 885b0a0b9036cb..ab0e87cde000e7 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getContent.php @@ -1,6 +1,6 @@ 'mock', - 'categories' => 'mock', + 'font_families' => array( 'mock' ), + 'categories' => array( 'mock' ), ); return array( @@ -47,14 +47,14 @@ public function mock_request( $preempt, $args, $url ) { } /** - * @dataProvider data_should_get_config_and_data + * @dataProvider data_should_get_content * * @param array $config Font collection config options. - * @param array $expected_data Expected data. + * @param array $expected_data Expected output data. */ - public function test_should_get_config_and_data( $config, $expected_data ) { + public function test_should_get_content( $config, $expected_data ) { $collection = new WP_Font_Collection( $config ); - $this->assertSame( $expected_data, $collection->get_config_and_data() ); + $this->assertSame( $expected_data, $collection->get_content() ); } /** @@ -62,12 +62,12 @@ public function test_should_get_config_and_data( $config, $expected_data ) { * * @return array[] */ - public function data_should_get_config_and_data() { + public function data_should_get_content() { $mock_file = wp_tempnam( 'my-collection-data-' ); - file_put_contents( $mock_file, '{"this is mock data":true}' ); + file_put_contents( $mock_file, '{"font_families":[ "mock" ], "categories":[ "mock" ] }' ); return array( - 'with a file' => array( + 'with a file' => array( 'config' => array( 'slug' => 'my-collection', 'name' => 'My Collection', @@ -75,13 +75,11 @@ public function data_should_get_config_and_data() { 'src' => $mock_file, ), 'expected_data' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'data' => array( 'this is mock data' => true ), + 'font_families' => array( 'mock' ), + 'categories' => array( 'mock' ), ), ), - 'with a url' => array( + 'with a url' => array( 'config' => array( 'slug' => 'my-collection-with-url', 'name' => 'My Collection with URL', @@ -89,27 +87,33 @@ public function data_should_get_config_and_data() { 'src' => 'https://localhost/fonts/mock-font-collection.json', ), 'expected_data' => array( - 'slug' => 'my-collection-with-url', - 'name' => 'My Collection with URL', - 'description' => 'My collection description', - 'data' => array( - 'fontFamilies' => 'mock', - 'categories' => 'mock', - ), + 'font_families' => array( 'mock' ), + 'categories' => array( 'mock' ), ), ), - 'with data' => array( + 'with font_families and categories' => array( 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'data' => array( 'this is mock data' => true ), + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( 'mock' ), + 'categories' => array( 'mock' ), ), 'expected_data' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'data' => array( 'this is mock data' => true ), + 'font_families' => array( 'mock' ), + 'categories' => array( 'mock' ), + ), + ), + 'with font_families without categories' => array( + 'config' => array( + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( 'mock' ), + ), + 'expected_data' => array( + 'font_families' => array( 'mock' ), + 'categories' => array(), ), ), ); diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php b/phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php new file mode 100644 index 00000000000000..7cfdfc829ab863 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php @@ -0,0 +1,103 @@ +assertTrue( WP_Font_Collection::is_config_valid( $config ) ); + } + + public function data_is_config_valid() { + return array( + 'with src' => array( + 'config' => array( + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => 'my-collection-data.json', + ), + ), + 'with font families' => array( + 'config' => array( + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( 'mock' ), + ), + ), + + ); + } + + /** + * @dataProvider data_is_config_valid_should_call_doing_ti_wrong + * + * @param mixed $config Config of the font collection. + */ + public function test_is_config_valid_should_call_doing_ti_wrong( $config ) { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid', 'Should call _doing_it_wrong if the config is not valid.' ); + $this->assertFalse( WP_Font_Collection::is_config_valid( $config ), 'Should return false if the config is not valid.' ); + } + + public function data_is_config_valid_should_call_doing_ti_wrong() { + return array( + 'with missing slug' => array( + 'config' => array( + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => 'my-collection-data.json', + ), + ), + 'with missing name' => array( + 'config' => array( + 'slug' => 'my-collection', + 'description' => 'My collection description', + 'src' => 'my-collection-data.json', + ), + ), + 'with missing src' => array( + 'config' => array( + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + ), + ), + 'with both src and font_families' => array( + 'config' => array( + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => 'my-collection-data.json', + 'font_families' => array( 'mock' ), + ), + ), + 'without src or font_families' => array( + 'config' => array( + 'slug' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + ), + ), + 'with empty config' => array( + 'config' => array(), + ), + 'without an array' => array( + 'config' => 'not an array', + ), + ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php b/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php index 3a1e387c3651b2..cee0628dad3103 100644 --- a/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php +++ b/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php @@ -30,11 +30,11 @@ public function test_should_initialize_data() { } /** - * @dataProvider data_should_throw_exception + * @dataProvider data_should_do_it_wrong * * @param mixed $font_data Data to test. */ - public function test_should_throw_exception( $font_data ) { + public function test_should_do_it_wrong( $font_data ) { $this->expectException( 'Exception' ); $this->expectExceptionMessage( 'Font family data is missing the slug.' ); @@ -46,7 +46,7 @@ public function test_should_throw_exception( $font_data ) { * * @return array */ - public function data_should_throw_exception() { + public function data_should_do_it_wrong() { return array( 'no slug' => array( array( diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index a7ea2870957e9d..b06ae3c8d53548 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -29,9 +29,9 @@ public function test_should_return_error_if_slug_is_missing() { 'description' => 'My Collection Description', 'src' => 'my-collection-data.json', ); - $this->expectException( 'Exception' ); - $this->expectExceptionMessage( 'Font Collection config slug is required as a non-empty string.' ); - WP_Font_Library::register_font_collection( $config ); + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); + $collection = WP_Font_Library::register_font_collection( $config ); + $this->assertWPError( $collection, 'A WP_Error should be returned.' ); } public function test_should_return_error_if_name_is_missing() { @@ -40,16 +40,16 @@ public function test_should_return_error_if_name_is_missing() { 'description' => 'My Collection Description', 'src' => 'my-collection-data.json', ); - $this->expectException( 'Exception' ); - $this->expectExceptionMessage( 'Font Collection config name is required as a non-empty string.' ); - WP_Font_Library::register_font_collection( $config ); + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); + $collection = WP_Font_Library::register_font_collection( $config ); + $this->assertWPError( $collection, 'A WP_Error should be returned.' ); } public function test_should_return_error_if_config_is_empty() { $config = array(); - $this->expectException( 'Exception' ); - $this->expectExceptionMessage( 'Font Collection config options is required as a non-empty array.' ); - WP_Font_Library::register_font_collection( $config ); + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); + $collection = WP_Font_Library::register_font_collection( $config ); + $this->assertWPError( $collection, 'A WP_Error should be returned.' ); } public function test_should_return_error_if_slug_is_repeated() { diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php new file mode 100644 index 00000000000000..164f88f3f7b4b2 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -0,0 +1,163 @@ +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, '{"font_families": [ "mock" ], "categories": [ "mock" ] }' ); + + wp_register_font_collection( + array( + 'name' => 'My Collection', + 'slug' => 'mock-col-slug', + 'src' => $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[\/\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 intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); + } + + /** + * @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->assertEquals( 200, $response->get_status() ); + } + + /** + * @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->assertEquals( 200, $response->get_status(), 'Response code is not 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( 'font_collection_not_found', $response, 404 ); + } + + /** + * @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, 'Response code 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, 'Response code 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(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not use test_get_item_schema(). + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php deleted file mode 100644 index 2469d71dc79ce8..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php +++ /dev/null @@ -1,42 +0,0 @@ -factory->user->create( - array( - 'role' => 'administrator', - ) - ); - wp_set_current_user( $admin_id ); - } - - /** - * Tear down each test method. - */ - public function tear_down() { - parent::tear_down(); - - // Reset $collections static property of WP_Font_Library class. - $reflection = new ReflectionClass( 'WP_Font_Library' ); - $property = $reflection->getProperty( 'collections' ); - $property->setAccessible( true ); - $property->setValue( null, array() ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php deleted file mode 100644 index c9d003389997b4..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php +++ /dev/null @@ -1,126 +0,0 @@ - 'one-collection', - 'name' => 'One Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => $mock_file, - ); - wp_register_font_collection( $config_with_file ); - - $config_with_url = array( - 'slug' => 'collection-with-url', - 'name' => 'Another Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => 'https://wordpress.org/fonts/mock-font-collection.json', - ); - - wp_register_font_collection( $config_with_url ); - - $config_with_non_existing_file = array( - 'slug' => 'collection-with-non-existing-file', - 'name' => 'Another Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => '/home/non-existing-file.json', - ); - - wp_register_font_collection( $config_with_non_existing_file ); - - $config_with_non_existing_url = array( - 'slug' => 'collection-with-non-existing-url', - 'name' => 'Another Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => 'https://non-existing-url-1234x.com.ar/fake-path/missing-file.json', - ); - - wp_register_font_collection( $config_with_non_existing_url ); - } - - public function mock_request( $preempt, $args, $url ) { - // Check if it's the URL you want to mock. - if ( 'https://wordpress.org/fonts/mock-font-collection.json' === $url ) { - - // Mock the response body. - $mock_collection_data = array( - 'fontFamilies' => 'mock', - 'categories' => 'mock', - ); - - return array( - 'body' => json_encode( $mock_collection_data ), - 'response' => array( - 'code' => 200, - ), - ); - } - - // For any other URL, return false which ensures the request is made as usual (or you can return other mock data). - return false; - } - - public function tear_down() { - // Remove the mock to not affect other tests. - remove_filter( 'pre_http_request', array( $this, 'mock_request' ) ); - - parent::tear_down(); - } - - public function test_get_font_collection_from_file() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/one-collection' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' ); - $this->assertSame( array( 'this is mock data' => true ), $data['data'], 'The response data does not have the expected file data.' ); - } - - public function test_get_font_collection_from_url() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-url' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' ); - } - - public function test_get_non_existing_collection_should_return_404() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/non-existing-collection-id' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 404, $response->get_status() ); - } - - public function test_get_non_existing_file_should_return_500() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-non-existing-file' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 500, $response->get_status() ); - } - - public function test_get_non_existing_url_should_return_500() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-non-existing-url' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 500, $response->get_status() ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php deleted file mode 100644 index 0a8d24e8f392ba..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php +++ /dev/null @@ -1,45 +0,0 @@ -dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); - $this->assertSame( array(), $response->get_data() ); - } - - public function test_get_font_collections() { - // Mock font collection data file. - $mock_file = wp_tempnam( 'my-collection-data-' ); - file_put_contents( $mock_file, '{"this is mock data":true}' ); - - // Add a font collection. - $config = array( - 'slug' => 'my-font-collection', - 'name' => 'My Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => $mock_file, - ); - wp_register_font_collection( $config ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertCount( 1, $data, 'The response data is not an array with one element.' ); - $this->assertArrayHasKey( 'slug', $data[0], 'The response data does not have the key with the collection slug.' ); - $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php deleted file mode 100644 index fb100a400fb4cf..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php +++ /dev/null @@ -1,24 +0,0 @@ -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[\/\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 intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); - } -} From 1698fb65482aad37d542e1793fa77d1ceacf6734 Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Fri, 19 Jan 2024 13:55:29 -0600 Subject: [PATCH 15/16] Fix font asset download when font faces are installed (#58021) --- .../global-styles/font-library-modal/font-collection.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 793ca9c753f337..8c029c92a9ef53 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -160,11 +160,10 @@ function FontCollection( { slug } ) { if ( fontFamily?.fontFace ) { await Promise.all( fontFamily.fontFace.map( async ( fontFace ) => { - if ( fontFace.downloadFromUrl ) { + if ( fontFace.src ) { fontFace.file = await downloadFontFaceAsset( - fontFace.downloadFromUrl + fontFace.src ); - delete fontFace.downloadFromUrl; } } ) ); From c89db29809d3133fc311ba7649d199a93545f612 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 19 Jan 2024 18:36:33 -0300 Subject: [PATCH 16/16] conditional download of assets --- .../font-library-modal/font-collection.js | 10 +++++- packages/editor/src/store/defaults.js | 32 ++++++++++--------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 8c029c92a9ef53..bc075a11dfd3dd 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -17,6 +17,8 @@ import { import { debounce } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { search, closeSmall } from '@wordpress/icons'; +import { store as editorStore } from '@wordpress/editor'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -37,6 +39,12 @@ const DEFAULT_CATEGORY = { name: __( 'All' ), }; function FontCollection( { slug } ) { + const fontLibraryDownloadAssets = useSelect( + ( select ) => + select( editorStore ).getEditorSettings().fontLibraryDownloadAssets, + [] + ); + const requiresPermission = slug === 'default-font-collection'; const getGoogleFontsPermissionFromStorage = () => { @@ -157,7 +165,7 @@ function FontCollection( { slug } ) { const fontFamily = fontsToInstall[ 0 ]; try { - if ( fontFamily?.fontFace ) { + if ( fontFamily?.fontFace && fontLibraryDownloadAssets ) { await Promise.all( fontFamily.fontFace.map( async ( fontFace ) => { if ( fontFace.src ) { diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index e4f86b3a7dfb21..173c0a3c132114 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -6,21 +6,22 @@ import { SETTINGS_DEFAULTS } from '@wordpress/block-editor'; /** * The default post editor settings. * - * @property {boolean|Array} allowedBlockTypes Allowed block types - * @property {boolean} richEditingEnabled Whether rich editing is enabled or not - * @property {boolean} codeEditingEnabled Whether code editing is enabled or not - * @property {boolean} fontLibraryEnabled Whether the font library is enabled or not. - * @property {boolean} enableCustomFields Whether the WordPress custom fields are enabled or not. - * true = the user has opted to show the Custom Fields panel at the bottom of the editor. - * false = the user has opted to hide the Custom Fields panel at the bottom of the editor. - * undefined = the current environment does not support Custom Fields, so the option toggle in Preferences -> Panels to enable the Custom Fields panel is not displayed. - * @property {number} autosaveInterval How often in seconds the post will be auto-saved via the REST API. - * @property {number} localAutosaveInterval How often in seconds the post will be backed up to sessionStorage. - * @property {Array?} availableTemplates The available post templates - * @property {boolean} disablePostFormats Whether or not the post formats are disabled - * @property {Array?} allowedMimeTypes List of allowed mime types and file extensions - * @property {number} maxUploadFileSize Maximum upload file size - * @property {boolean} supportsLayout Whether the editor supports layouts. + * @property {boolean|Array} allowedBlockTypes Allowed block types + * @property {boolean} richEditingEnabled Whether rich editing is enabled or not + * @property {boolean} codeEditingEnabled Whether code editing is enabled or not + * @property {boolean} fontLibraryEnabled Whether the font library is enabled or not. + * @property {boolean} fontLibraryDownloadAssets Whether the font library is enabled or not. + * @property {boolean} enableCustomFields Whether the WordPress custom fields are enabled or not. + * true = the user has opted to show the Custom Fields panel at the bottom of the editor. + * false = the user has opted to hide the Custom Fields panel at the bottom of the editor. + * undefined = the current environment does not support Custom Fields, so the option toggle in Preferences -> Panels to enable the Custom Fields panel is not displayed. + * @property {number} autosaveInterval How often in seconds the post will be auto-saved via the REST API. + * @property {number} localAutosaveInterval How often in seconds the post will be backed up to sessionStorage. + * @property {Array?} availableTemplates The available post templates + * @property {boolean} disablePostFormats Whether or not the post formats are disabled + * @property {Array?} allowedMimeTypes List of allowed mime types and file extensions + * @property {number} maxUploadFileSize Maximum upload file size + * @property {boolean} supportsLayout Whether the editor supports layouts. */ export const EDITOR_SETTINGS_DEFAULTS = { ...SETTINGS_DEFAULTS, @@ -28,6 +29,7 @@ export const EDITOR_SETTINGS_DEFAULTS = { richEditingEnabled: true, codeEditingEnabled: true, fontLibraryEnabled: true, + fontLibraryDownloadAssets: true, enableCustomFields: undefined, defaultRenderingMode: 'post-only', };