diff --git a/lib/experimental/fonts-library/class-wp-font-family-utils.php b/lib/experimental/fonts-library/class-wp-font-family-utils.php
new file mode 100644
index 00000000000000..ef239936b8e5a3
--- /dev/null
+++ b/lib/experimental/fonts-library/class-wp-font-family-utils.php
@@ -0,0 +1,92 @@
+data = $font_data;
+ }
+
+ /**
+ * Gets the font family data.
+ *
+ * @since 6.4.0
+ *
+ * @return array An array in fontFamily theme.json format.
+ */
+ public function get_data() {
+ return $this->data;
+ }
+
+ /**
+ * Gets the font family data.
+ *
+ * @since 6.4.0
+ *
+ * @return string fontFamily in theme.json format as stringified JSON.
+ */
+ public function get_data_as_json() {
+ return wp_json_encode( $this->get_data() );
+ }
+
+ /**
+ * Checks whether the font family has font faces defined.
+ *
+ * @since 6.4.0
+ *
+ * @return bool True if the font family has font faces defined, false otherwise.
+ */
+ public function has_font_faces() {
+ return ! empty( $this->data['fontFace'] ) && is_array( $this->data['fontFace'] );
+ }
+
+ /**
+ * Removes font family assets.
+ *
+ * @since 6.4.0
+ *
+ * @return bool True if assets were removed, false otherwise.
+ */
+ private function remove_font_family_assets() {
+ if ( $this->has_font_faces() ) {
+ foreach ( $this->data['fontFace'] as $font_face ) {
+ $were_assets_removed = $this->delete_font_face_assets( $font_face );
+ if ( false === $were_assets_removed ) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Removes a font family from the database and deletes its assets.
+ *
+ * @since 6.4.0
+ *
+ * @return bool|WP_Error True if the font family was uninstalled, WP_Error otherwise.
+ */
+ public function uninstall() {
+ $post = $this->get_data_from_post();
+ if ( null === $post ) {
+ return new WP_Error(
+ 'font_family_not_found',
+ __( 'The font family could not be found.', 'gutenberg' )
+ );
+ }
+
+ if (
+ ! $this->remove_font_family_assets() ||
+ ! wp_delete_post( $post->ID, true )
+ ) {
+ return new WP_Error(
+ 'font_family_not_deleted',
+ __( 'The font family could not be deleted.', 'gutenberg' )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Deletes a specified font asset file from the fonts directory.
+ *
+ * @since 6.4.0
+ *
+ * @param string $src The path of the font asset file to delete.
+ * @return bool Whether the file was deleted.
+ */
+ private static function delete_asset( $src ) {
+ $filename = basename( $src );
+ $file_path = path_join( WP_Fonts_Library::get_fonts_dir(), $filename );
+
+ wp_delete_file( $file_path );
+
+ return ! file_exists( $file_path );
+ }
+
+ /**
+ * Deletes all font face asset files associated with a given font face.
+ *
+ * @since 6.4.0
+ *
+ * @param array $font_face The font face array containing the 'src' attribute
+ * with the file path(s) to be deleted.
+ * @return bool True if delete was successful, otherwise false.
+ */
+ private static function delete_font_face_assets( $font_face ) {
+ $sources = (array) $font_face['src'];
+ foreach ( $sources as $src ) {
+ $was_asset_removed = self::delete_asset( $src );
+ if ( ! $was_asset_removed ) {
+ // Bail if any of the assets could not be removed.
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Gets the overrides for the 'wp_handle_upload' function.
+ *
+ * @since 6.4.0
+ *
+ * @param string $filename The filename to be used for the uploaded file.
+ * @return array The overrides for the 'wp_handle_upload' function.
+ */
+ private function get_upload_overrides( $filename ) {
+ return 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' => false,
+ 'unique_filename_callback' => static function() use ( $filename ) {
+ // Keep the original filename.
+ return $filename;
+ },
+ );
+ }
+
+ /**
+ * Downloads a font asset from a specified source URL and saves it to
+ * the font directory.
+ *
+ * @since 6.4.0
+ *
+ * @param string $url The source URL of the font asset to be downloaded.
+ * @param string $filename The filename to save the downloaded font asset as.
+ * @return string|bool The relative path to the downloaded font asset.
+ * False if the download failed.
+ */
+ private function download_asset( $url, $filename ) {
+ // Checks if the file to be downloaded has a font mime type.
+ if ( ! WP_Font_Family_Utils::has_font_mime_type( $filename ) ) {
+ return false;
+ }
+
+ // Include file with download_url() if function doesn't exist.
+ if ( ! function_exists( 'download_url' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ }
+
+ // Downloads the font asset or returns false.
+ $temp_file = download_url( $url );
+ if ( is_wp_error( $temp_file ) ) {
+ return false;
+ }
+
+ $overrides = $this->get_upload_overrides( $filename );
+
+ $file = array(
+ 'tmp_name' => $temp_file,
+ 'name' => $filename,
+ );
+
+ $handled_file = wp_handle_upload( $file, $overrides );
+
+ // Cleans the temp file.
+ @unlink( $temp_file );
+
+ if ( ! isset( $handled_file['url'] ) ) {
+ return false;
+ }
+
+ // Returns the relative path to the downloaded font asset to be used as
+ // font face src.
+ return $handled_file['url'];
+ }
+
+ /**
+ * Moves an uploaded font face asset from temp folder to the fonts directory.
+ *
+ * This is used when uploading local fonts.
+ *
+ * @since 6.4.0
+ *
+ * @param array $font_face Font face to download.
+ * @param array $file Uploaded file.
+ * @return array New font face with all assets downloaded and referenced in
+ * the font face definition.
+ */
+ private function move_font_face_asset( $font_face, $file ) {
+ $new_font_face = $font_face;
+ $filename = WP_Font_Family_Utils::get_filename_from_font_face(
+ $this->data['slug'],
+ $font_face,
+ $file['name']
+ );
+
+ // Remove the uploaded font asset reference from the font face definition
+ // because it is no longer needed.
+ unset( $new_font_face['uploaded_file'] );
+
+ // If the filename has no font mime type, don't move the file and
+ // return the font face definition without src to be ignored later.
+ if ( ! WP_Font_Family_Utils::has_font_mime_type( $filename ) ) {
+ return $new_font_face;
+ }
+
+ // 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';
+ }
+
+ $overrides = $this->get_upload_overrides( $filename );
+
+ $handled_file = wp_handle_upload( $file, $overrides );
+
+ if ( isset( $handled_file['url'] ) ) {
+ // If the file was successfully moved, update the font face definition
+ // to reference the new file location.
+ $new_font_face['src'] = $handled_file['url'];
+ }
+
+ return $new_font_face;
+ }
+
+ /**
+ * Sanitizes the font family data using WP_Theme_JSON.
+ *
+ * @since 6.4.0
+ *
+ * @return array A sanitized font family definition.
+ */
+ private function sanitize() {
+ // Creates the structure of theme.json array with the new fonts.
+ $fonts_json = array(
+ 'version' => '2',
+ 'settings' => array(
+ 'typography' => array(
+ 'fontFamilies' => array( $this->data ),
+ ),
+ ),
+ );
+ // Creates a new WP_Theme_JSON object with the new fonts to
+ // leverage sanitization and validation.
+ $theme_json = new WP_Theme_JSON_Gutenberg( $fonts_json );
+ $theme_data = $theme_json->get_data();
+ $sanitized_font = ! empty( $theme_data['settings']['typography']['fontFamilies'] )
+ ? $theme_data['settings']['typography']['fontFamilies'][0]
+ : array();
+ $this->data = $sanitized_font;
+ return $this->data;
+ }
+
+ /**
+ * Downloads font face assets.
+ *
+ * Downloads the font face asset(s) associated with a font face. It works with
+ * both single source URLs and arrays of multiple source URLs.
+ *
+ * @since 6.4.0
+ *
+ * @param array $font_face The font face array containing the 'src' attribute
+ * with the source URL(s) of the assets.
+ * @return array The modified font face array with the new source URL(s) to
+ * the downloaded assets.
+ */
+ private function download_font_face_assets( $font_face ) {
+ $new_font_face = $font_face;
+ $sources = (array) $font_face['download_from_url'];
+ $new_font_face['src'] = array();
+ $index = 0;
+
+ foreach ( $sources as $src ) {
+ $suffix = $index++ > 0 ? $index : '';
+ $filename = WP_Font_Family_Utils::get_filename_from_font_face(
+ $this->data['slug'],
+ $font_face,
+ $src,
+ $suffix
+ );
+ $new_src = $this->download_asset( $src, $filename );
+ if ( $new_src ) {
+ $new_font_face['src'][] = $new_src;
+ }
+ }
+
+ if ( count( $new_font_face['src'] ) === 1 ) {
+ $new_font_face['src'] = $new_font_face['src'][0];
+ }
+
+ // Remove the download url reference from the font face definition
+ // because it is no longer needed.
+ unset( $new_font_face['download_from_url'] );
+
+ return $new_font_face;
+ }
+
+
+ /**
+ * Downloads font face assets if the font family is a Google font,
+ * or moves them if it is a local font.
+ *
+ * @since 6.4.0
+ *
+ * @param array $files An array of files to be installed.
+ * @return bool True if the font faces were downloaded or moved successfully, false otherwise.
+ */
+ private function download_or_move_font_faces( $files ) {
+ if ( ! $this->has_font_faces() ) {
+ return true;
+ }
+
+ $new_font_faces = array();
+ foreach ( $this->data['fontFace'] as $font_face ) {
+ // If the fonts are not meant to be dowloaded or uploaded
+ // (for example to install fonts that use a remote url).
+ $new_font_face = $font_face;
+
+ // If installing google fonts, download the font face assets.
+ if ( ! empty( $font_face['download_from_url'] ) ) {
+ $new_font_face = $this->download_font_face_assets( $new_font_face );
+ }
+
+ // If installing local fonts, move the font face assets from
+ // the temp folder to the wp fonts directory.
+ if ( ! empty( $font_face['uploaded_file'] ) && ! empty( $files ) ) {
+ $new_font_face = $this->move_font_face_asset(
+ $new_font_face,
+ $files[ $new_font_face['uploaded_file'] ]
+ );
+ }
+
+ /*
+ * If the font face assets were successfully downloaded, add the font face
+ * to the new font. Font faces with failed downloads are not added to the
+ * new font.
+ */
+ if ( ! empty( $new_font_face['src'] ) ) {
+ $new_font_faces[] = $new_font_face;
+ }
+ }
+
+ if ( ! empty( $new_font_faces ) ) {
+ $this->data['fontFace'] = $new_font_faces;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets the post for a font family.
+ *
+ * @since 6.4.0
+ *
+ * @return WP_Post|null The post for this font family object or
+ * null if the post does not exist.
+ */
+ public function get_font_post() {
+ $args = array(
+ 'post_type' => 'wp_font_family',
+ 'post_name' => $this->data['slug'],
+ 'name' => $this->data['slug'],
+ 'posts_per_page' => 1,
+ );
+
+ $posts_query = new WP_Query( $args );
+
+ if ( $posts_query->have_posts() ) {
+ return $posts_query->posts[0];
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the data for this object from the database and
+ * sets it to the data property.
+ *
+ * @since 6.4.0
+ *
+ * @return WP_Post|null The post for this font family object or
+ * null if the post does not exist.
+ */
+ private function get_data_from_post() {
+ $post = $this->get_font_post();
+ if ( $post ) {
+ $this->data = json_decode( $post->post_content, true );
+ return $post;
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a post for a font family.
+ *
+ * @since 6.4.0
+ *
+ * @return int|WP_Error Post ID if the post was created, WP_Error otherwise.
+ */
+ private function create_font_post() {
+ $post = array(
+ 'post_title' => $this->data['name'],
+ 'post_name' => $this->data['slug'],
+ 'post_type' => 'wp_font_family',
+ 'post_content' => $this->get_data_as_json(),
+ 'post_status' => 'publish',
+ );
+
+ $post_id = wp_insert_post( $post );
+ if ( 0 === $post_id || is_wp_error( $post_id ) ) {
+ return new WP_Error(
+ 'font_post_creation_failed',
+ __( 'Font post creation failed', 'gutenberg' )
+ );
+ }
+
+ return $post_id;
+ }
+
+ /**
+ * Updates a post for a font family.
+ *
+ * @since 6.4.0
+ *
+ * @param WP_Post $post The post to update.
+ * @return int|WP_Error Post ID if the update was successful, WP_Error otherwise.
+ */
+ private function update_font_post( $post ) {
+ $post_font_data = json_decode( $post->post_content, true );
+ $new_data = WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data );
+ $this->data = $new_data;
+
+ $post = array(
+ 'ID' => $post->ID,
+ 'post_content' => $this->get_data_as_json(),
+ );
+
+ $post_id = wp_update_post( $post );
+
+ if ( 0 === $post_id || is_wp_error( $post_id ) ) {
+ return new WP_Error(
+ 'font_post_update_failed',
+ __( 'Font post update failed', 'gutenberg' )
+ );
+ }
+
+ return $post_id;
+ }
+
+ /**
+ * Creates a post for a font in the fonts library if it doesn't exist,
+ * or updates it if it does.
+ *
+ * @since 6.4.0
+ *
+ * @return int|WP_Error Post id if the post was created or updated successfully,
+ * WP_Error otherwise.
+ */
+ private function create_or_update_font_post() {
+ $this->sanitize();
+
+ $post = $this->get_font_post();
+ if ( $post ) {
+ return $this->update_font_post( $post );
+ }
+
+ return $this->create_font_post();
+ }
+
+ /**
+ * Installs the font family into the library.
+ *
+ * @since 6.4.0
+ *
+ * @param array $files Optional. An array of files to be installed. Default null.
+ * @return array|WP_Error An array of font family data on success, WP_Error otherwise.
+ */
+ public function install( $files = null ) {
+ add_filter( 'upload_dir', array( 'WP_Fonts_Library', 'set_upload_dir' ) );
+ $were_assets_written = $this->download_or_move_font_faces( $files );
+ remove_filter( 'upload_dir', array( 'WP_Fonts_Library', 'set_upload_dir' ) );
+
+ if ( ! $were_assets_written ) {
+ return new WP_Error(
+ 'font_face_download_failed',
+ __( 'The font face assets could not be written.', 'gutenberg' )
+ );
+ }
+
+ $post_id = $this->create_or_update_font_post();
+
+ if ( is_wp_error( $post_id ) ) {
+ return $post_id;
+ }
+
+ return $this->get_data();
+ }
+}
diff --git a/lib/experimental/fonts-library/class-wp-fonts-library.php b/lib/experimental/fonts-library/class-wp-fonts-library.php
new file mode 100644
index 00000000000000..f0482ae9a864af
--- /dev/null
+++ b/lib/experimental/fonts-library/class-wp-fonts-library.php
@@ -0,0 +1,64 @@
+ 'font/otf',
+ 'ttf' => 'font/ttf',
+ 'woff' => 'font/woff',
+ 'woff2' => 'font/woff2',
+ );
+
+ /**
+ * Gets the upload directory for fonts.
+ *
+ * @since 6.4.0
+ *
+ * @return string Path of the upload directory for fonts.
+ */
+ public static function get_fonts_dir() {
+ return wp_upload_dir()['basedir'] . '/fonts';
+ }
+
+ /**
+ * Sets the upload directory for fonts.
+ *
+ * @since 6.4.0
+ *
+ * @param array $defaults {
+ * Default upload directory.
+ *
+ * @type string $path Path to the directory.
+ * @type string $url URL for the directory.
+ * @type string $subdir Sub-directory of the directory.
+ * @type string $basedir Base directory.
+ * @type string $baseurl Base URL.
+ * }
+ * @return array Modified upload directory.
+ */
+ public static function set_upload_dir( $defaults ) {
+ $defaults['subdir'] = '/fonts';
+ $defaults['path'] = $defaults['basedir'] . $defaults['subdir'];
+ $defaults['url'] = $defaults['baseurl'] . $defaults['subdir'];
+
+ return $defaults;
+ }
+}
diff --git a/lib/experimental/fonts-library/class-wp-rest-fonts-library-controller.php b/lib/experimental/fonts-library/class-wp-rest-fonts-library-controller.php
new file mode 100644
index 00000000000000..c49560ea32c160
--- /dev/null
+++ b/lib/experimental/fonts-library/class-wp-rest-fonts-library-controller.php
@@ -0,0 +1,332 @@
+rest_base = 'fonts';
+ $this->namespace = 'wp/v2';
+ }
+
+ /**
+ * Registers the routes for the objects of the controller.
+ *
+ * @since 6.4.0
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'install_fonts' ),
+ 'permission_callback' => array( $this, 'update_fonts_library_permissions_check' ),
+ 'args' => array(
+ 'fontFamilies' => array(
+ 'required' => true,
+ 'type' => 'string',
+ 'validate_callback' => array( $this, 'validate_install_font_families' ),
+ ),
+ ),
+ ),
+ )
+ );
+
+ 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_fonts_library_permissions_check' ),
+ 'args' => $this->uninstall_schema(),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns validation errors in font families data for installation.
+ *
+ * @since 6.4.0
+ *
+ * @param array[] $font_families Font families to install.
+ * @param array $files Files to install.
+ * @return array $error_messages Array of error messages.
+ */
+ private function get_validation_errors( $font_families, $files ) {
+ $error_messages = array();
+
+ if ( ! is_array( $font_families ) ) {
+ $error_messages[] = __( 'fontFamilies should be an array of font families.', 'gutenberg' );
+ return $error_messages;
+ }
+
+ // Checks if there is at least one font family.
+ if ( count( $font_families ) < 1 ) {
+ $error_messages[] = __( 'fontFamilies should have at least one font family definition.', 'gutenberg' );
+ return $error_messages;
+ }
+
+ for ( $family_index = 0; $family_index < count( $font_families ); $family_index++ ) {
+ $font_family = $font_families[ $family_index ];
+
+ if (
+ ! isset( $font_family['slug'] ) ||
+ ! isset( $font_family['name'] ) ||
+ ! isset( $font_family['fontFamily'] )
+ ) {
+ $error_messages[] = sprintf(
+ // translators: 1: font family index.
+ __( 'Font family [%s] should have slug, name and fontFamily properties defined.', 'gutenberg' ),
+ $family_index
+ );
+ }
+
+ if ( isset( $font_family['fontFace'] ) ) {
+ if ( ! is_array( $font_family['fontFace'] ) ) {
+ $error_messages[] = sprintf(
+ // translators: 1: font family index.
+ __( 'Font family [%s] should have fontFace property defined as an array.', 'gutenberg' ),
+ $family_index
+ );
+ }
+
+ if ( count( $font_family['fontFace'] ) < 1 ) {
+ $error_messages[] = sprintf(
+ // translators: 1: font family index.
+ __( 'Font family [%s] should have at least one font face definition.', 'gutenberg' ),
+ $family_index
+ );
+ }
+
+ if ( ! empty( $font_family['fontFace'] ) ) {
+ for ( $face_index = 0; $face_index < count( $font_family['fontFace'] ); $face_index++ ) {
+
+ $font_face = $font_family['fontFace'][ $face_index ];
+ if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) {
+ $error_messages[] = sprintf(
+ // translators: 1: font family index, 2: font face index.
+ __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ),
+ $family_index,
+ $face_index
+ );
+ }
+
+ if ( isset( $font_face['download_from_url'] ) && isset( $font_face['uplodaded_file'] ) ) {
+ $error_messages[] = sprintf(
+ // translators: 1: font family index, 2: font face index.
+ __( 'Font family [%1$s] Font face [%2$s] should have only one of the download_from_url or uploaded_file properties defined and not both.', 'gutenberg' ),
+ $family_index,
+ $face_index
+ );
+ }
+
+ if ( isset( $font_face['uploaded_file'] ) ) {
+ if ( ! isset( $files[ $font_face['uploaded_file'] ] ) ) {
+ $error_messages[] = sprintf(
+ // translators: 1: font family index, 2: font face index.
+ __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.', 'gutenberg' ),
+ $family_index,
+ $face_index
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $error_messages;
+ }
+
+ /**
+ * Validate input for the install endpoint.
+ *
+ * @since 6.4.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.
+ */
+ public function validate_install_font_families( $param, $request ) {
+ $font_families = json_decode( $param, true );
+ $files = $request->get_file_params();
+ $error_messages = $this->get_validation_errors( $font_families, $files );
+
+ if ( empty( $error_messages ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) );
+ }
+
+ /**
+ * Gets the schema for the uninstall endpoint.
+ *
+ * @since 6.4.0
+ *
+ * @return array Schema array.
+ */
+ public function uninstall_schema() {
+ return array(
+ 'fontFamilies' => array(
+ 'type' => 'array',
+ 'description' => __( 'The font families to install.', 'gutenberg' ),
+ 'required' => true,
+ 'minItems' => 1,
+ 'items' => array(
+ 'required' => true,
+ 'type' => 'object',
+ 'properties' => array(
+ 'slug' => array(
+ 'type' => 'string',
+ 'description' => __( 'The font family slug.', 'gutenberg' ),
+ 'required' => true,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Removes font families from the fonts library and all their assets.
+ *
+ * @since 6.4.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 uninstall_fonts( $request ) {
+ $fonts_param = $request->get_param( 'fontFamilies' );
+
+ foreach ( $fonts_param as $font_data ) {
+ $font = new WP_Font_Family( $font_data );
+ $result = $font->uninstall();
+
+ // If there was an error uninstalling the font, return the error.
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ }
+
+ return new WP_REST_Response( __( 'Font family uninstalled successfully.', 'gutenberg' ), 200 );
+ }
+
+ /**
+ * Checks whether the user has permissions to update the fonts library.
+ *
+ * @since 6.4.0
+ *
+ * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
+ */
+ public function update_fonts_library_permissions_check() {
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return new WP_Error(
+ 'rest_cannot_update_fonts_library',
+ __( 'Sorry, you are not allowed to update the fonts library on this site.', 'gutenberg' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
+ }
+
+ // The update endpoints requires write access to the temp and the fonts directories.
+ $temp_dir = get_temp_dir();
+ $upload_dir = wp_upload_dir()['basedir'];
+ if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) {
+ return new WP_Error(
+ 'rest_cannot_write_fonts_folder',
+ __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ),
+ array(
+ 'status' => 500,
+ )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Installs new fonts.
+ *
+ * Takes a request containing new fonts to install, downloads their assets, and adds them
+ * to the fonts library.
+ *
+ * @since 6.4.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 fonts library post content.
+ */
+ public function install_fonts( $request ) {
+ // Get new fonts to install.
+ $fonts_param = $request->get_param( 'fontFamilies' );
+
+ /*
+ * 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.
+ */
+ $fonts_to_install = json_decode( $fonts_param, true );
+
+ if ( empty( $fonts_to_install ) ) {
+ return new WP_Error(
+ 'no_fonts_to_install',
+ __( 'No fonts to install', 'gutenberg' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Get uploaded files (used when installing local fonts).
+ $files = $request->get_file_params();
+
+ // Iterates the fonts data received and creates a new WP_Font_Family object for each one.
+ $fonts_installed = array();
+ foreach ( $fonts_to_install as $font_data ) {
+ $font = new WP_Font_Family( $font_data );
+ $font->install( $files );
+ $fonts_installed[] = $font;
+ }
+
+ if ( empty( $fonts_installed ) ) {
+ return new WP_Error(
+ 'error_installing_fonts',
+ __( 'Error installing fonts. No font was installed.', 'gutenberg' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ $response = array();
+ foreach ( $fonts_installed as $font ) {
+ $response[] = $font->get_data();
+ }
+
+ return new WP_REST_Response( $response );
+ }
+}
diff --git a/lib/experimental/fonts-library/fonts-library.php b/lib/experimental/fonts-library/fonts-library.php
new file mode 100644
index 00000000000000..359442f231fc70
--- /dev/null
+++ b/lib/experimental/fonts-library/fonts-library.php
@@ -0,0 +1,37 @@
+ true,
+ 'label' => 'Font Library',
+ 'show_in_rest' => true,
+ );
+ register_post_type( 'wp_font_family', $args );
+
+ // @core-merge: This code will go into Core's `create_initial_rest_routes()`.
+ $fonts_library_controller = new WP_REST_Fonts_Library_Controller();
+ $fonts_library_controller->register_routes();
+}
+
+add_action( 'rest_api_init', 'gutenberg_init_fonts_library' );
+
diff --git a/lib/load.php b/lib/load.php
index 85e9f9575e6e6f..e7da8de58c1b48 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -28,7 +28,7 @@ function gutenberg_is_experiment_enabled( $name ) {
return ! empty( $experiments[ $name ] );
}
-// These files only need to be loaded if within a rest server instance
+// These files only need to be loaded if within a rest server instance.
// which this class will exist if that is the case.
if ( class_exists( 'WP_REST_Controller' ) ) {
if ( ! class_exists( 'WP_REST_Block_Editor_Settings_Controller' ) ) {
@@ -144,7 +144,14 @@ function gutenberg_is_experiment_enabled( $name ) {
* the Font Face (redesigned Fonts API) to be merged before the Fonts Library while
* keeping Fonts API available for sites that are using it.
*/
-if ( class_exists( 'WP_Fonts_Library' ) || class_exists( 'WP_Fonts_Library_Controller' ) ) {
+if ( defined( 'FONTS_LIBRARY_ENABLE' ) && FONTS_LIBRARY_ENABLE ) {
+ // Loads the Fonts Library.
+ require __DIR__ . '/experimental/fonts-library/class-wp-fonts-library.php';
+ require __DIR__ . '/experimental/fonts-library/class-wp-font-family-utils.php';
+ require __DIR__ . '/experimental/fonts-library/class-wp-font-family.php';
+ require __DIR__ . '/experimental/fonts-library/class-wp-rest-fonts-library-controller.php';
+ require __DIR__ . '/experimental/fonts-library/fonts-library.php';
+
if ( ! class_exists( 'WP_Font_Face' ) ) {
require __DIR__ . '/experimental/fonts/class-wp-font-face.php';
require __DIR__ . '/experimental/fonts/class-wp-font-face-resolver.php';
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 2bc6b7b29900de..5cf01a02fef695 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -9,6 +9,7 @@
>
+
@@ -18,6 +19,7 @@
ms-required
+ fontsapi
diff --git a/phpunit/fonts-library/class-wp-font-family-test.php b/phpunit/fonts-library/class-wp-font-family-test.php
new file mode 100644
index 00000000000000..85e5c2c6e5fdec
--- /dev/null
+++ b/phpunit/fonts-library/class-wp-font-family-test.php
@@ -0,0 +1,340 @@
+ 'Piazzolla',
+ 'name' => 'Piazzolla',
+ );
+ $this->expectException( 'Exception' );
+ $this->expectExceptionMessage( 'Font family data is missing the slug.' );
+ new WP_Font_Family( $font_data );
+ }
+
+ /**
+ * Tests that data is set by the constructor and retrieved by the get_data() method.
+ *
+ * @covers ::__construct
+ * @covers ::get_data
+ *
+ * @dataProvider data_font_fixtures
+ *
+ * @param array $font_data Font family data in theme.json format.
+ */
+ public function test_get_data( $font_data ) {
+ $font = new WP_Font_Family( $font_data );
+ $this->assertSame( $font_data, $font->get_data() );
+ }
+
+ /**
+ * Tests that the get_data_as_json() method returns the expected data in JSON format.
+ *
+ * @covers ::get_data_as_json
+ *
+ * @dataProvider data_get_data_as_json
+ *
+ * @param array $font_data Font family data in theme.json format.
+ * @param string $expected Expected font family data as JSON string.
+ */
+ public function test_get_data_as_json( $font_data, $expected ) {
+ $font = new WP_Font_Family( $font_data );
+ $this->assertSame( $expected, $font->get_data_as_json() );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_get_data_as_json() {
+ return array(
+ 'piazzolla' => array(
+ 'font_data' => array(
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'src' => 'https://example.com/fonts/piazzolla_italic_400.ttf',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ ),
+ ),
+ ),
+ 'expected' => '{"slug":"piazzolla","fontFamily":"Piazzolla","name":"Piazzolla","fontFace":[{"fontFamily":"Piazzolla","src":"https:\/\/example.com\/fonts\/piazzolla_italic_400.ttf","fontStyle":"italic","fontWeight":"400"}]}',
+ ),
+ 'piazzolla2' => array(
+ 'font_data' => array(
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'src' => 'https://example.com/fonts/piazzolla_italic_400.ttf',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ ),
+ ),
+ ),
+ 'expected' => '{"slug":"piazzolla","fontFamily":"Piazzolla","name":"Piazzolla","fontFace":[{"fontFamily":"Piazzolla","src":"https:\/\/example.com\/fonts\/piazzolla_italic_400.ttf","fontStyle":"italic","fontWeight":"400"}]}',
+ ),
+ );
+ }
+
+ /**
+ * Tests that the has_font_faces() method correctly determines whether a font family has font faces.
+ *
+ * @covers ::has_font_faces
+ *
+ * @dataProvider data_has_font_faces
+ *
+ * @param array $font_data Font family data in theme.json format.
+ * @param bool $expected Expected result.
+ */
+ public function test_has_font_faces( $font_data, $expected ) {
+ $font = new WP_Font_Family( $font_data );
+ $this->assertSame( $expected, $font->has_font_faces() );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_has_font_faces() {
+ return array(
+ 'with font faces' => array(
+ 'font_data' => array(
+ 'slug' => 'piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ ),
+ ),
+ ),
+ 'expected' => true,
+ ),
+
+ 'empty font faces' => array(
+ 'font_data' => array(
+ 'slug' => 'piazzolla',
+ 'fontFace' => array(),
+ ),
+ 'expected' => false,
+ ),
+
+ 'without font faces' => array(
+ 'font_data' => array(
+ 'slug' => 'piazzolla',
+ ),
+ 'expected' => false,
+ ),
+ );
+ }
+
+ /**
+ * Tests that the install() and uninstall() methods work as expected
+ * It uses different types of font families: with local, remote or no files.
+ *
+ * @covers ::install
+ * @covers ::uninstall
+ * @covers ::get_font_post
+ *
+ * @dataProvider data_font_fixtures
+ *
+ * @param array $font_data Font family data in theme.json format.
+ * @param array $installed_font_data Font family data in theme.json format expected data after installation.
+ * @param array $files_data Optional. Files data in $_FILES format (Used only if the font has local files). Default: empty array.
+ */
+ public function test_install_and_uninstall( $font_data, $installed_font_data, $files_data = array() ) {
+ $font = new WP_Font_Family( $font_data );
+ $font->install( $files_data );
+
+ // Check that the post was created.
+ $post = $font->get_font_post();
+ $this->assertInstanceof( 'WP_Post', $post, 'The font post was not created.' );
+
+ // Check that the post has the correct data.
+ $this->assertSame( $installed_font_data['name'], $post->post_title, 'The font post has the wrong title.' );
+ $this->assertSame( $installed_font_data['slug'], $post->post_name, 'The font post has the wrong slug.' );
+
+ $content = json_decode( $post->post_content, true );
+ $this->assertSame( $installed_font_data['fontFamily'], $content['fontFamily'], 'The font post content has the wrong font family.' );
+ $this->assertSame( $installed_font_data['slug'], $content['slug'], 'The font post content has the wrong slug.' );
+
+ $this->assertArrayNotHasKey( 'download_from_url', $content, 'The installed font should not have the url from where it was downloaded.' );
+ $this->assertArrayNotHasKey( 'uploaded_file', $content, 'The installed font should not have the reference to the file from it was installed.' );
+
+ $this->assertCount( count( $installed_font_data['fontFace'] ), $content['fontFace'], 'One or more font faces could not be installed.' );
+
+ $font->uninstall();
+
+ // Check that the post was deleted.
+ $post = $font->get_font_post();
+ $this->assertNull( $post, 'The font post was not deleted' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_font_fixtures() {
+ $temp_file_path1 = wp_tempnam( 'Inter-' );
+ file_put_contents( $temp_file_path1, 'Mocking file content' );
+ $temp_file_path2 = wp_tempnam( 'Inter-' );
+ file_put_contents( $temp_file_path2, 'Mocking file content' );
+
+ return array(
+ 'with_one_google_font_face_to_be_downloaded' => array(
+ 'font_data' => array(
+ 'name' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ 'download_from_url' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ ),
+ ),
+ ),
+ 'installed_font_data' => array(
+ 'name' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'piazzolla_italic_400.ttf', // This is just filename of the font asset and not the entire URL because we can't know the URL of the asset in the test.
+ ),
+ ),
+ ),
+ 'files_data' => null,
+ ),
+ 'with_one_google_font_face_to_not_be_downloaded' => array(
+ 'font_data' => array(
+ 'name' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ ),
+ ),
+ ),
+ 'installed_font_data' => array(
+ 'name' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'piazzolla_italic_400.ttf', // This is just filename of the font asset and not the entire URL because we can't know the URL of the asset in the test.
+ ),
+ ),
+ ),
+ 'files_data' => null,
+ ),
+ 'without_font_faces' => array(
+ 'font_data' => array(
+ 'name' => 'Arial',
+ 'slug' => 'arial',
+ 'fontFamily' => 'Arial',
+ 'fontFace' => array(),
+ ),
+ 'installed_font_data' => array(
+ 'name' => 'Arial',
+ 'slug' => 'arial',
+ 'fontFamily' => 'Arial',
+ 'fontFace' => array(),
+ ),
+ 'files_data' => null,
+ ),
+ 'with_local_files' => array(
+ 'font_data' => array(
+ 'name' => 'Inter',
+ 'slug' => 'inter',
+ 'fontFamily' => 'Inter',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'uploaded_file' => 'files0',
+ ),
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '500',
+ 'uploaded_file' => 'files1',
+ ),
+ ),
+ ),
+ 'installed_font_data' => array(
+ 'name' => 'Inter',
+ 'slug' => 'inter',
+ 'fontFamily' => 'Inter',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => 'inter_normal_400.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '500',
+ 'src' => 'inter_normal_500.ttf',
+ ),
+ ),
+ ),
+ 'files_data' => array(
+ 'files0' => array(
+ 'name' => 'inter1.ttf',
+ 'type' => 'font/ttf',
+ 'tmp_name' => $temp_file_path1,
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ 'files1' => array(
+ 'name' => 'inter2.ttf',
+ 'type' => 'font/ttf',
+ 'tmp_name' => $temp_file_path2,
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpunit/fonts-library/class-wp-font-family-utils-test.php b/phpunit/fonts-library/class-wp-font-family-utils-test.php
new file mode 100644
index 00000000000000..775c88ce8b596c
--- /dev/null
+++ b/phpunit/fonts-library/class-wp-font-family-utils-test.php
@@ -0,0 +1,320 @@
+assertSame( $expected, WP_Font_Family_Utils::has_font_mime_type( $font_file ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_has_font_mime_type_fixtures() {
+ return array(
+ 'ttf' => array(
+ 'font_file' => '/temp/piazzolla_400_italic.ttf',
+ 'expected' => true,
+ ),
+ 'otf' => array(
+ 'font_file' => '/temp/piazzolla_400_italic.otf',
+ 'expected' => true,
+ ),
+ 'woff' => array(
+ 'font_file' => '/temp/piazzolla_400_italic.woff',
+ 'expected' => true,
+ ),
+ 'woff2' => array(
+ 'font_file' => '/temp/piazzolla_400_italic.woff2',
+ 'expected' => true,
+ ),
+ 'exe' => array(
+ 'font_file' => '/temp/piazzolla_400_italic.exe',
+ 'expected' => false,
+ ),
+ 'php' => array(
+ 'font_file' => '/temp/piazzolla_400_italic.php',
+ 'expected' => false,
+ ),
+ );
+ }
+
+ /**
+ * @covers ::get_filename_from_font_face
+ *
+ * @dataProvider data_get_filename_from_font_face_fixtures
+ *
+ * @param string $slug Font slug.
+ * @param array $font_face Font face data in theme.json format.
+ * @param string $suffix Suffix added to the resulting filename. Default empty string.
+ * @param string $expected_file_name Expected file name.
+ */
+ public function test_get_filename_from_font_face( $slug, $font_face, $suffix, $expected_file_name ) {
+
+ $this->assertSame(
+ $expected_file_name,
+ WP_Font_Family_Utils::get_filename_from_font_face(
+ $slug,
+ $font_face,
+ $font_face['src'],
+ $suffix
+ )
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_get_filename_from_font_face_fixtures() {
+ return array(
+ 'piazzolla' => array(
+ 'slug' => 'piazzolla',
+ 'font_face' => array(
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/font_file.ttf',
+ ),
+ 'suffix' => '',
+ 'expected_file_name' => 'piazzolla_italic_400.ttf',
+ ),
+ 'inter' => array(
+ 'slug' => 'inter',
+ 'font_face' => array(
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/font_file.otf',
+ ),
+ 'suffix' => '',
+ 'expected_file_name' => 'inter_normal_600.otf',
+ ),
+ );
+ }
+
+ /**
+ * @covers ::merge_fonts_data
+ *
+ * @dataProvider data_merge_fonts_data_fixtures
+ *
+ * @param bool $are_mergeable Whether the fonts are mergeable.
+ * @param array $font1 First font data in theme.json format.
+ * @param array $font2 Second font data in theme.json format.
+ * @param array $expected_result Expected result.
+ */
+ public function test_merge_fonts_data( $are_mergeable, $font1, $font2, $expected_result ) {
+ // Fonts with same slug should be merged.
+ $merged_font = WP_Font_Family_Utils::merge_fonts_data( $font1, $font2 );
+
+ if ( $are_mergeable ) {
+ $this->assertNotWPError( $merged_font, 'Fonts could not be merged' );
+ $this->assertSame( $expected_result, $merged_font, 'The font family data and font faces merged not as expected' );
+ } else {
+ $this->assertWPError( $merged_font, 'Merging non mergeable fonts (diifferent slug) should have failed.' );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_merge_fonts_data_fixtures() {
+ return array(
+
+ 'mergeable_fonts' => array(
+ 'are_mergeable' => true,
+ 'font1' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ ),
+ ),
+ 'font2' => array(
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '700',
+ 'src' => 'http://example.com/fonts/piazzolla_700_normal.ttf',
+ ),
+ ),
+ ),
+ 'expected_result' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '700',
+ 'src' => 'http://example.com/fonts/piazzolla_700_normal.ttf',
+ ),
+ ),
+ ),
+ ),
+
+ 'mergeable_fonts_with_repeated_font_faces' => array(
+ 'are_mergeable' => true,
+ 'font1' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ ),
+ ),
+ 'font2' => array(
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ ),
+ ),
+ 'expected_result' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf',
+ ),
+ ),
+ ),
+ ),
+
+ 'non_mergeable_fonts' => array(
+ 'are_mergeable' => false,
+ 'font1' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ ),
+ ),
+ 'font2' => array(
+ 'slug' => 'inter',
+ 'fontFamily' => 'Inter',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '700',
+ 'src' => 'http://example.com/fonts/inter_700_normal.ttf',
+ ),
+ ),
+ ),
+ 'expected_result' => 'WP_Error',
+ ),
+
+ );
+ }
+
+}
diff --git a/phpunit/fonts-library/class-wp-fonts-library-test.php b/phpunit/fonts-library/class-wp-fonts-library-test.php
new file mode 100644
index 00000000000000..5e802fb6e3685d
--- /dev/null
+++ b/phpunit/fonts-library/class-wp-fonts-library-test.php
@@ -0,0 +1,52 @@
+assertStringEndsWith( '/wp-content/uploads/fonts', WP_Fonts_Library::get_fonts_dir() );
+ }
+
+ /**
+ * @covers ::set_upload_dir
+ *
+ * @dataProvider data_set_upload_dir
+ *
+ * @param array $defaults Default upload directory data.
+ * @param array $expected Modified upload directory data.
+ */
+ public function test_set_upload_dir( $defaults, $expected ) {
+ $this->assertSame( $expected, WP_Fonts_Library::set_upload_dir( $defaults ) );
+ }
+
+ public function data_set_upload_dir() {
+ return array(
+ 'fonts_subdir' => array(
+ 'defaults' => array(
+ 'subdir' => '/abc',
+ 'basedir' => '/var/www/html/wp-content/uploads',
+ 'baseurl' => 'http://example.com/wp-content/uploads',
+ ),
+ 'expected' => array(
+ 'subdir' => '/fonts',
+ 'basedir' => '/var/www/html/wp-content/uploads',
+ 'baseurl' => 'http://example.com/wp-content/uploads',
+ 'path' => '/var/www/html/wp-content/uploads/fonts',
+ 'url' => 'http://example.com/wp-content/uploads/fonts',
+ ),
+ ),
+ );
+ }
+
+}
diff --git a/phpunit/fonts-library/class-wp-rest-fonts-library-controller-test.php b/phpunit/fonts-library/class-wp-rest-fonts-library-controller-test.php
new file mode 100644
index 00000000000000..ddf61aeb41ea6c
--- /dev/null
+++ b/phpunit/fonts-library/class-wp-rest-fonts-library-controller-test.php
@@ -0,0 +1,343 @@
+user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ }
+
+ /**
+ * @covers ::register_routes
+ */
+ public function test_register_routes() {
+ $routes = rest_get_server()->get_routes();
+ $this->assertArrayHasKey( '/wp/v2/fonts', $routes, 'Rest server has not the fonts path intialized.' );
+ $this->assertCount( 2, $routes['/wp/v2/fonts'], 'Rest server has not the 2 fonts paths initialized.' );
+ $this->assertArrayHasKey( 'POST', $routes['/wp/v2/fonts'][0]['methods'], 'Rest server has not the POST method for fonts intialized.' );
+ $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/fonts'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' );
+ }
+
+ /**
+ * @covers ::uninstall_fonts
+ */
+ public function test_uninstall_non_existing_fonts() {
+ wp_set_current_user( self::$admin_id );
+ $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
+
+ $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( 'fontFamilies', $non_existing_font_data );
+ $response = rest_get_server()->dispatch( $uninstall_request );
+ $response->get_data();
+ $this->assertSame( 500, $response->get_status(), 'The response status is not 500.' );
+ }
+
+
+ /**
+ * @covers ::install_fonts
+ * @covers ::uninstall_fonts
+ *
+ * @dataProvider data_install_and_uninstall_fonts
+ *
+ * @param array $font_families Font families to install in theme.json format.
+ * @param array $files Font files to install.
+ * @param array $expected_response Expected response data.
+ */
+ public function test_install_and_uninstall_fonts( $font_families, $files, $expected_response ) {
+ wp_set_current_user( self::$admin_id );
+ $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
+ $font_families_json = json_encode( $font_families );
+ $install_request->set_param( 'fontFamilies', $font_families_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 ), $data, 'Not all the font families were installed correctly.' );
+
+ // Checks that the font families were installed correctly.
+ for ( $family_index = 0; $family_index < count( $data ); $family_index++ ) {
+ $installed_font = $data[ $family_index ];
+ $expected_font = $expected_response[ $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.
+ $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'] );
+ }
+ }
+
+ // Compares if the rest of the data is the same.
+ $this->assertEquals( $expected_font, $installed_font, 'The endpoint answer is not as expected.' );
+ }
+
+ $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
+ $uninstall_request->set_param( 'fontFamilies', $font_families );
+ $response = rest_get_server()->dispatch( $uninstall_request );
+ $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
+ }
+
+ /**
+ * Data provider for test_install_and_uninstall_fonts
+ */
+ public function data_install_and_uninstall_fonts() {
+
+ $temp_file_path1 = wp_tempnam( 'Piazzola1-' );
+ file_put_contents( $temp_file_path1, 'Mocking file content' );
+ $temp_file_path2 = wp_tempnam( 'Monteserrat-' );
+ file_put_contents( $temp_file_path2, 'Mocking file content' );
+
+ return array(
+
+ 'google_fonts_to_download' => array(
+ 'font_families' => 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',
+ 'download_from_url' => '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',
+ 'download_from_url' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf',
+ ),
+ ),
+ ),
+ ),
+ 'files' => array(),
+ 'expected_response' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => '/wp-content/uploads/fonts/piazzolla_normal_400.ttf',
+ ),
+ ),
+ ),
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'src' => '/wp-content/uploads/fonts/montserrat_normal_100.ttf',
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ 'google_fonts_to_use_as_is' => array(
+ 'font_families' => 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',
+ ),
+ ),
+ ),
+ 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',
+ ),
+ ),
+ ),
+ ),
+ 'files' => array(),
+ 'expected_response' => 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',
+ ),
+ ),
+ ),
+ 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',
+
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ 'fonts_without_font_faces' => array(
+ 'font_families' => array(
+ array(
+ 'fontFamily' => 'Arial',
+ 'slug' => 'arial',
+ 'name' => 'Arial',
+ ),
+ ),
+ 'files' => array(),
+ 'expected_response' => array(
+ array(
+ 'fontFamily' => 'Arial',
+ 'slug' => 'arial',
+ 'name' => 'Arial',
+ ),
+ ),
+ ),
+
+ 'fonts_with_local_fonts_assets' => array(
+ 'font_families' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'uploaded_file' => 'files0',
+ ),
+ ),
+ ),
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'uploaded_file' => 'files1',
+ ),
+ ),
+ ),
+ ),
+ '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(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => '/wp-content/uploads/fonts/piazzolla_normal_400.ttf',
+ ),
+ ),
+ ),
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'src' => '/wp-content/uploads/fonts/montserrat_normal_100.ttf',
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpunit/multisite.xml b/phpunit/multisite.xml
index 7a6117948547d3..23de57aa1d9bd8 100644
--- a/phpunit/multisite.xml
+++ b/phpunit/multisite.xml
@@ -10,6 +10,7 @@
+
@@ -19,6 +20,7 @@
ms-excluded
+ fontsapi