diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 090ee61d5187a..6cf838cb54519 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -735,4 +735,7 @@ // Font management. add_action( 'wp_head', 'wp_print_font_faces', 50 ); +// Font Library. +add_action( 'init', 'wp_register_default_font_collection' ); + unset( $filter, $action ); diff --git a/src/wp-includes/fonts.php b/src/wp-includes/fonts.php index 306364bdc8099..bfff198b0150a 100644 --- a/src/wp-includes/fonts.php +++ b/src/wp-includes/fonts.php @@ -51,3 +51,37 @@ function wp_print_font_faces( $fonts = array() ) { $wp_font_face = new WP_Font_Face(); $wp_font_face->generate_and_print( $fonts ); } + +/** + * Registers a new Font Collection in the Font Library. + * + * @since 6.4.0 + * + * @param string[] $config { + * Font collection associative array of configuration options. + * + * @type string $id The font collection's unique ID. + * @type string $src The font collection's data JSON file. + * } + * @return WP_Font_Collection|WP_Error A font collection is it was registered + * successfully, else WP_Error. + */ +function wp_register_font_collection( $config ) { + return WP_Font_Library::register_font_collection( $config ); +} + +/** + * Registers the default fonts collection for the Font Library. + * + * @since 6.4.0 + */ +function wp_register_default_font_collection() { + wp_register_font_collection( + array( + 'id' => 'default-font-collection', + 'name' => 'Google Fonts', + 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.' ), + 'src' => 'https://s.w.org/images/fonts/16.7/collections/google-fonts-with-preview.json', + ) + ); +} diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php new file mode 100644 index 0000000000000..c8cdb7879a68d --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -0,0 +1,109 @@ +config = $config; + } + + /** + * Gets the font collection config. + * + * @since 6.4.0 + * + * @return array An array containing the font collection config. + */ + public function get_config() { + return $this->config; + } + + /** + * Gets the font collection data. + * + * @since 6.4.0 + * + * @return array|WP_Error An array containing the list of font families in theme.json format on success, + * else an instance of WP_Error on failure. + */ + public function get_data() { + // 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'] ) ) { + return new WP_Error( 'font_collection_read_error', __( 'Invalid URL for font collection data.' ) ); + } + + $response = wp_remote_get( $this->config['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.' ) ); + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( empty( $data ) ) { + return new WP_Error( 'font_collection_read_error', __( 'Error decoding the font collection data from the REST response JSON.' ) ); + } + // If the src is a file path, read the data from the file. + } else { + if ( ! file_exists( $this->config['src'] ) ) { + return new WP_Error( 'font_collection_read_error', __( 'Font collection data JSON file does not exist.' ) ); + } + $data = wp_json_file_decode( $this->config['src'], array( 'associative' => true ) ); + if ( empty( $data ) ) { + return new WP_Error( 'font_collection_read_error', __( 'Error reading the font collection data JSON file contents.' ) ); + } + } + + $collection_data = $this->get_config(); + $collection_data['data'] = $data; + unset( $collection_data['src'] ); + + return $collection_data; + } +} diff --git a/src/wp-includes/fonts/class-wp-font-family-utils.php b/src/wp-includes/fonts/class-wp-font-family-utils.php new file mode 100644 index 0000000000000..b5a246a66d300 --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-family-utils.php @@ -0,0 +1,89 @@ +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_post_by_slug(); + + if ( is_null( $post ) ) { + return new WP_Error( + 'font_family_not_found', + __( 'The font family could not be found.' ) + ); + } + + $this->data = json_decode( $post->post_content, true ); + + 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.' ) + ); + } + + 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_Font_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' => true, + 'mimes' => WP_Font_Library::get_font_mime_types(), + '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['uploadedFile'] ); + + /* + * 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( $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['downloadFromUrl']; + $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['downloadFromUrl'] ); + + 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; + + $font_face_is_repeated = false; + + // If the font face has the same fontStyle and fontWeight as an existing, continue. + foreach ( $new_font_faces as $font_to_compare ) { + if ( $new_font_face['fontStyle'] === $font_to_compare['fontStyle'] && + $new_font_face['fontWeight'] === $font_to_compare['fontWeight'] ) { + $font_face_is_repeated = true; + } + } + + if ( $font_face_is_repeated ) { + continue; + } + + // If installing google fonts, download the font face assets. + if ( ! empty( $font_face['downloadFromUrl'] ) ) { + $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['uploadedFile'] ) && ! empty( $files ) ) { + $new_font_face = $this->move_font_face_asset( + $new_font_face, + $files[ $new_font_face['uploadedFile'] ] + ); + } + + /* + * 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 a post for a font family by its slug. + * + * @since 6.4.0 + * + * @return WP_Post|null The post if the post exists, null otherwise. + */ + public function get_post_by_slug() { + $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 null; + } + + $post = $posts_query->posts[0]; + return $post; + } + + /** + * 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.' ) + ); + } + return $post_id; + } + + /** + * Gets the font faces that are in both the existing and incoming font families. + * + * @since 6.4.0 + * + * @param array $existing The existing font faces. + * @param array $incoming The incoming font faces. + * @return array The font faces that are in both the existing and incoming font families. + */ + private function get_intersecting_font_faces( $existing, $incoming ) { + $intersecting = array(); + foreach ( $existing as $existing_face ) { + foreach ( $incoming as $incoming_face ) { + if ( $incoming_face['fontStyle'] === $existing_face['fontStyle'] && + $incoming_face['fontWeight'] === $existing_face['fontWeight'] && + $incoming_face['src'] !== $existing_face['src'] ) { + $intersecting[] = $existing_face; + } + } + } + + return $intersecting; + } + + /** + * 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 ); + + // Handles the case where the post content is not valid JSON. + $new_data = $post_font_data + ? WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data ) + : $this->data; + + if ( ! empty( $post_font_data['fontFace'] ) ) { + $intersecting = $this->get_intersecting_font_faces( $post_font_data['fontFace'], $new_data['fontFace'] ); + } + + if ( ! empty( $intersecting ) ) { + $serialized_font_faces = array_map( 'serialize', $new_data['fontFace'] ); + $serialized_intersecting = array_map( 'serialize', $intersecting ); + + $diff = array_diff( $serialized_font_faces, $serialized_intersecting ); + + $new_data['fontFace'] = array_values( array_map( 'unserialize', $diff ) ); + + foreach ( $intersecting as $intersect ) { + $this->delete_font_face_assets( $intersect ); + } + } + $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' ) + ); + } + + return $post_id; + } + + /** + * Creates a post for a font in the Font 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_post_by_slug(); + + 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 WP_Post|WP_Error The font family post if the installation was successful, WP_Error otherwise. + */ + public function install( $files = null ) { + add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_dir', array( 'WP_Font_Library', 'set_upload_dir' ) ); + $were_assets_written = $this->download_or_move_font_faces( $files ); + remove_filter( 'upload_dir', array( 'WP_Font_Library', 'set_upload_dir' ) ); + remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + + if ( ! $were_assets_written ) { + return new WP_Error( + 'font_face_download_failed', + __( 'The font face assets could not be written.' ) + ); + } + + $result = $this->create_or_update_font_post(); + if ( is_wp_error( $result ) ) { + return $result; + } + $post = get_post( $result ); + return $post; + } +} diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php new file mode 100644 index 0000000000000..91cab9215e078 --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -0,0 +1,147 @@ += 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; + + return array( + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : $php_7_ttf_mime_type, + 'woff' => PHP_VERSION_ID >= 80100 ? 'font/woff' : 'application/font-woff', + 'woff2' => PHP_VERSION_ID >= 80100 ? 'font/woff2' : 'application/font-woff2', + ); + } + + /** + * Registers a new font collection. + * + * @since 6.4.0 + * + * @param array $config Font collection config options. + * See {@see wp_register_font_collection()} for the supported fields. + * @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 ) { + $new_collection = new WP_Font_Collection( $config ); + + if ( isset( self::$collections[ $config['id'] ] ) ) { + return new WP_Error( 'font_collection_registration_error', __( 'Font collection already registered.' ) ); + } + + self::$collections[ $config['id'] ] = $new_collection; + + return $new_collection; + } + + /** + * Gets all the font collections available. + * + * @since 6.4.0 + * + * @return array List of font collections. + */ + public static function get_font_collections() { + return self::$collections; + } + + /** + * Gets a font collection. + * + * @since 6.4.0 + * + * @param string $id Font collection id. + * @return array List of font collections. + */ + public static function get_font_collection( $id ) { + if ( array_key_exists( $id, self::$collections ) ) { + return self::$collections[ $id ]; + } + + return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); + } + + /** + * 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 path_join( WP_CONTENT_DIR, '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['basedir'] = WP_CONTENT_DIR; + $defaults['baseurl'] = content_url(); + $defaults['subdir'] = '/fonts'; + $defaults['path'] = self::get_fonts_dir(); + $defaults['url'] = $defaults['baseurl'] . '/fonts'; + + return $defaults; + } + + /** + * Sets the allowed mime types for fonts. + * + * @since 6.4.0 + * + * @param array $mime_types List of allowed mime types. + * @return array Modified upload directory. + */ + public static function set_allowed_mime_types( $mime_types ) { + return array_merge( $mime_types, self::get_font_mime_types() ); + } +} diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 8a3bc56656163..d5f85acf058fa 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -558,6 +558,36 @@ function create_initial_post_types() { ) ); + register_post_type( + 'wp_font_family', + array( + 'label' => _x( 'Font Family', 'post type general name' ), + 'description' => __( 'Font Family definition for installed fonts.' ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + '_edit_link' => '/site-editor.php?canvas=edit', /* internal use only. don't use this when registering your own post type. */ + 'show_ui' => false, + 'show_in_rest' => false, + 'rewrite' => false, + 'hierarchical' => false, + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => true, + 'supports' => array( + 'title', + 'slug', + 'editor', + ), + ) + ); + register_post_status( 'publish', array( diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a8a2aac177288..5e22e4f3b7e99 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -385,6 +385,14 @@ function create_initial_rest_routes() { // Navigation Fallback. $controller = new WP_REST_Navigation_Fallback_Controller(); $controller->register_routes(); + + // Font Families. + $controller = new WP_REST_Font_Families_Controller(); + $controller->register_routes(); + + // Font Collections. + $controller = new WP_REST_Font_Collections_Controller(); + $controller->register_routes(); } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php new file mode 100644 index 0000000000000..641547137445e --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php @@ -0,0 +1,334 @@ +rest_base = 'font-collections'; + $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::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + 'schema' => array( $this, 'get_items_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\/\w-]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.' ), + 'type' => 'string', + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Gets a font collection. + * + * @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 get_item( $request ) { + $id = $request['id']; + $collection = WP_Font_Library::get_font_collection( $id ); + // If the collection doesn't exist returns a 404. + if ( is_wp_error( $collection ) ) { + $collection->add_data( array( 'status' => 404 ) ); + return $collection; + } + $collection_with_data = $collection->get_data(); + // If there was an error getting the collection data, return the error. + if ( is_wp_error( $collection_with_data ) ) { + $collection_with_data->add_data( array( 'status' => 500 ) ); + return $collection_with_data; + } + + $response = $this->prepare_item_for_response( $collection_with_data, $request ); + return rest_ensure_response( $response ); + } + + /** + * Gets the font collections available. + * + * @since 6.4.0 + * + * @param WP_REST_Request $request Full details about the request. * + * @return WP_REST_Response Response object. + * + */ + public function get_items( $request ) { + $collections = array(); + foreach ( WP_Font_Library::get_font_collections() as $collection ) { + $collections[] = $this->prepare_item_for_response( $collection->get_config(), null ); + } + + return new WP_REST_Response( $collections, 200 ); + } + + /** + * Checks whether the user has permissions to update the Font 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_font_library_permissions_check() { + 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.' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + return true; + } + + /** + * Retrieves the schema for the font collections item, conforming to JSON Schema. + * + * @since 6.4.0 + * + * @return array Item schema data. + */ + public function get_items_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'font-collections', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Name of the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'description' => array( + 'description' => __( 'Description of the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'src' => array( + 'description' => __( 'Source to the list of font families.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ), + ); + + $this->schema = $schema; + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @since 6.4.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'font-collection', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Name of the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'description' => array( + 'description' => __( 'Description of the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'data' => array( + 'description' => __( 'Data of the font collection.' ), + 'type' => 'object', + 'context' => array( 'view', 'edit', 'embed' ), + 'properties' => array( + 'font_families' => array( + 'description' => __( 'List of font families.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( 'Name of the font family.' ), + 'type' => 'string', + ), + 'font_family' => array( + 'description' => __( 'Font family string.' ), + 'type' => 'string', + ), + 'slug' => array( + 'description' => __( 'Slug of the font family.' ), + 'type' => 'string', + ), + 'category' => array( + 'description' => __( 'Category of the font family.' ), + 'type' => 'string', + ), + 'font_face' => array( + 'description' => __( 'Font face details.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'download_from_url' => array( + 'description' => __( 'URL to download the font.' ), + 'type' => 'string', + ), + 'font_weight' => array( + 'description' => __( 'Font weight.' ), + 'type' => 'string', + ), + 'fonts_style' => array( + 'description' => __( 'Font style.' ), + 'type' => 'string', + ), + 'font_family' => array( + 'description' => __( 'Font family string.' ), + 'type' => 'string', + ), + 'preview' => array( + 'description' => __( 'URL for font preview.' ), + 'type' => 'string', + ), + ), + ), + ), + 'preview' => array( + 'description' => __( 'URL for font family preview.' ), + 'type' => 'string', + ), + ), + ), + ), + ), + ), + ), + ); + + $this->schema = $schema; + return $this->add_additional_fields_schema( $this->schema ); + } + /** + * Convert string from camelCase to snake_case. + * + * @since 6.4.0 + * + * @param string $input String to convert. + * @return string Converted string. + */ + private function camel_to_snake( $input ) { + $output = _wp_to_kebab_case( $input ); + return str_replace( '-', '_', $output ); + } + + /** + * Convert array keys from camelCase to snake_case. + * + * @since 6.4.0 + * + * @param array $array Array to convert. + * @return array Converted array. + */ + private function array_keys_to_snake_case( $array ) { + $new_array = []; + foreach ( $array as $key => $value ) { + $snake_key = $this->camel_to_snake( $key ); + // If the value is an array, recurse. + if ( is_array( $value ) ) { + $value = $this->array_keys_to_snake_case( $value ); + } + // Add to the new array + $new_array[ $snake_key ] = $value; + } + return $new_array; + } + + /** + * Prepares a single font collection output for response. + * + * @since 6.4.0 + * + * @param array $item Font collection data. + * @param WP_REST_Request $request Request object. + * @return array Font collection data. + */ + public function prepare_item_for_response( $item, $request ) { + return $this->array_keys_to_snake_case( $item ); + } + +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php new file mode 100644 index 0000000000000..0adb3ad33d73f --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php @@ -0,0 +1,518 @@ +rest_base = 'font-families'; + $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::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'args' => array( + 'slug' => array( + 'required' => true, + 'type' => 'string', + ), + 'name' => array( + 'required' => true, + 'type' => 'string', + ), + 'fontFamily' => array( + 'required' => true, + 'type' => 'string', + ), + 'fontFace' => array( + 'required' => false, + 'type' => 'string', + 'validate_callback' => array( $this, 'validate_font_faces' ), + ), + ), + ), + 'schema' => array( $this, 'get_items_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\/\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Get item (font family). + * + * @since 6.4.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $font_family = new WP_Font_Family( array( 'slug' => $request['slug'] ) ); + $post = $font_family->get_post_by_slug(); + if ( ! $post ) { + return new WP_Error( + 'font_family_slug_not_found', + __( 'Font Family with that slug was not found.' ), + array( + 'status' => 404, + ) + ); + } + $item = $this->prepare_item_for_response( $post, $request ); + if ( ! $item ) { + return new WP_Error( + 'font_family_invalid_json_content', + __( 'The JSON content of the font family is invalid.' ), + array( + 'status' => 500, + ) + ); + } + return rest_ensure_response( $item ); + } + + /** + * Get items (font families). + * + * @since 6.4.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $args = array( + 'post_type' => 'wp_font_family', + 'post_status' => 'publish', + 'posts_per_page' => $request['per_page'] ?? 10, + 'paged' => $request['page'] ?? 1, + 'orderby' => $request['orderby'] ?? 'name', + 'order' => $request['order'] ?? 'ASC', + ); + $posts = get_posts( $args ); + $response = array(); + foreach ( $posts as $post ) { + $item = $this->prepare_item_for_response( $post, $request ); + if ( $item ) { + $response[] = $item; + } + } + return rest_ensure_response( $response ); + } + + public function prepare_item_for_response( $post, $request ) { + return json_decode( $post->post_content, true ); + } + + /** + * Returns validation errors in font faces data for installation. + * + * @since 6.4.0 + * + * @param array[] $font_faces Font faces to install. + * @param array $files Files to install. + * @return WP_Error Validation errors. + */ + private function get_validation_errors( $font_faces, $files ) { + $error = new WP_Error(); + + if ( ! is_array( $font_faces ) ) { + $error->add( 'rest_invalid_param', __( 'fontFace should be an array.' ) ); + return $error; + } + + if ( count( $font_faces ) < 1 ) { + $error->add( 'rest_invalid_param', __( 'fontFace should have at least one item.' ) ); + return $error; + } + + for ( $face_index = 0; $face_index < count( $font_faces ); $face_index++ ) { + $font_face = $font_faces[ $face_index ]; + if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { + $error_message = sprintf( + // translators: 1: font face index. + __( 'Font face (%1$s) should have fontWeight and fontStyle properties defined.' ), + $face_index + ); + $error->add( 'rest_invalid_param', $error_message ); + } + + if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { + $error_message = sprintf( + // translators: 1: font face index. + __( 'Font face (%1$s) should have only one of the downloadFromUrl or uploadedFile properties defined and not both.' ), + $face_index + ); + $error->add( 'rest_invalid_param', $error_message ); + } + + if ( isset( $font_face['uploadedFile'] ) ) { + if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { + $error_message = sprintf( + // translators: 1: font face index. + __( 'Font face (%1$s) file is not defined in the request files.' ), + $face_index + ); + $error->add( 'rest_invalid_param', $error_message ); + } + } + } + return $error; + } + + /** + * Validate input for the install endpoint. + * + * @since 6.4.0 + * + * @param string $param The font faces to install. + * @param WP_REST_Request $request The request object. + * @return bool|WP_Error True if the parameter is valid, WP_Error otherwise. + */ + public function validate_font_faces( $param, $request ) { + $font_faces = json_decode( $param, true ); + if ( null === $font_faces ) { + return new WP_Error( + 'rest_invalid_param', + __( 'Invalid font faces parameter.' ), + array( 'status' => 400 ) + ); + } + + $files = $request->get_file_params(); + $validation = $this->get_validation_errors( $font_faces, $files ); + + if ( $validation->has_errors() ) { + $validation->add_data( array( 'status' => 400 ) ); + return $validation; + } + + return true; + } + + /** + * Removes font families from the Font 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 delete_item( $request ) { + + $font_family = new WP_Font_Family( array( 'slug' => $request['slug'] ) ); + $result = $font_family->uninstall(); + + if ( is_wp_error( $result ) ) { + if ( 'font_family_not_found' === $result->get_error_code() ) { + $result->add_data( array( 'status' => 404 ) ); + } else { + $result->add_data( array( 'status' => 500 ) ); + } + } + return rest_ensure_response( $result ); + } + + /** + * Checks whether the user has permissions to update the Font 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_font_library_permissions_check() { + 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.' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + return true; + } + + /** + * Checks whether the user has write permissions to the temp and fonts directories. + * + * @since 6.4.0 + * + * @return true|WP_Error True if the user has write permissions, WP_Error object otherwise. + */ + 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_Font_Library::get_fonts_dir(); + if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { + return false; + } + return true; + } + + /** + * Checks whether the request needs write permissions. + * + * @since 6.4.0 + * + * @param array $font_family Font family definition. + * @return bool Whether the request needs write permissions. + */ + private function needs_write_permission( $font_family ) { + if ( isset( $font_family['fontFace'] ) ) { + foreach ( $font_family['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; + } + } + } + return false; + } + + /** + * Installs new fonts. + * + * Takes a request containing new fonts to install, downloads their assets, and adds them + * to the Font 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 Font Library post content. + */ + public function create_item( $request ) { + $font_family_data = array( + 'slug' => $request->get_param( 'slug' ), + 'name' => $request->get_param( 'name' ), + 'fontFamily' => $request->get_param( 'fontFamily' ), + ); + + if ( $request->get_param( 'fontFace' ) ) { + $font_family_data['fontFace'] = json_decode( $request->get_param( 'fontFace' ), true ); + } + + if ( $this->needs_write_permission( $font_family_data ) && ! $this->has_write_permission() ) { + return new WP_Error( + 'cannot_write_fonts_folder', + __( 'Error: WordPress does not have permission to write the fonts folder on your server.' ), + array( + 'status' => 500, + ) + ); + } + + // Get uploaded files (used when installing local fonts). + $files = $request->get_file_params(); + $font_family = new WP_Font_Family( $font_family_data ); + $result = $font_family->install( $files ); + $response = $this->prepare_item_for_response( $result, $request ); + return rest_ensure_response( $response ); + } + + /** + * Retrieves the font family response schema + * + * @since 6.4.0 + * + * @return array Font Family schema data. + */ + private function font_family_schema() { + return array( + 'fontFace' => array( + 'description' => __( 'An array of font face objects.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'fontFamily' => array( + 'description' => __( 'Name of the font family.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'fontStyle' => array( + 'description' => __( 'Style of the font.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'fontWeight' => array( + 'description' => __( 'Weight of the font.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'src' => array( + 'description' => __( 'Source URL of the font.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ), + ), + 'fontFamily' => array( + 'description' => __( 'CSS string for the font family.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'name' => array( + 'description' => __( 'Display name of the font.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'slug' => array( + 'description' => __( 'Slug of the font.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ); + } + + /** + * Retrieves item schema + * + * @since 6.4.0 + * + * @return array Font family schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + $family_schema = array ( + 'title' => 'Font family', + 'type' => 'object', + 'properties' => $this->font_family_schema(), + ); + $delete_success_schema = array( + 'title' => 'Font family delete success', + 'type' => 'boolean', + 'description' => 'Indicates a successful response.', + 'enum' => array( true), + ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'Font family item endpoint reponse', + 'oneOf' => array( $family_schema, $delete_success_schema, $this->error_schema() ), + ); + $this->schema = $schema; + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the items schema. + * + * @since 6.4.0 + * + * @return array Font families schema data. + */ + public function get_items_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + $families_schema = array( + 'title' => 'Font families', + 'type' => 'array', + 'items' => ( + array( + 'type' => 'object', + 'properties' => $this->font_family_schema(), + ) + ) + ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'Font families items endpoint reponse', + 'oneOf' => array( $families_schema, $this->error_schema() ), + ); + $this->schema = $schema; + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the error response schema + * + * @since 6.4.0 + * + * @return array Error schema data. + */ + private function error_schema () { + return array( + 'title' => 'Error response', + 'type' => 'object', + 'properties' => array( + 'errors' => array( + 'type' => 'object', + 'description' => 'An associative array of error codes to error messages.', + 'propertyNames' => array( + 'type' => 'string', + ), + 'additionalProperties' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + 'error_data' => array( + 'type' => 'object', + 'description' => 'An associative array of error codes to mixed error data.', + 'propertyNames' => array( + 'type' => 'string', + ), + 'additionalProperties' => array( + 'type' => 'string', + ), + ), + ), + ); + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 528f335cb7c2a..d5a7045d86461 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -301,6 +301,8 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-templates-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-url-details-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-families-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-collections-controller.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php'; @@ -364,6 +366,10 @@ require ABSPATH . WPINC . '/fonts/class-wp-font-face-resolver.php'; require ABSPATH . WPINC . '/fonts/class-wp-font-face.php'; require ABSPATH . WPINC . '/fonts.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-collection.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-library.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-family-utils.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-family.php'; $GLOBALS['wp_embed'] = new WP_Embed(); diff --git a/tests/phpunit/data/fonts/DMSans.woff2 b/tests/phpunit/data/fonts/DMSans.woff2 new file mode 100644 index 0000000000000..9a7696df2ade0 Binary files /dev/null and b/tests/phpunit/data/fonts/DMSans.woff2 differ diff --git a/tests/phpunit/data/fonts/Merriweather.ttf b/tests/phpunit/data/fonts/Merriweather.ttf new file mode 100644 index 0000000000000..3fecc77777abf Binary files /dev/null and b/tests/phpunit/data/fonts/Merriweather.ttf differ diff --git a/tests/phpunit/data/fonts/cooper-hewitt.woff b/tests/phpunit/data/fonts/cooper-hewitt.woff new file mode 100644 index 0000000000000..8f395dec21501 Binary files /dev/null and b/tests/phpunit/data/fonts/cooper-hewitt.woff differ diff --git a/tests/phpunit/data/fonts/gilbert-color.otf b/tests/phpunit/data/fonts/gilbert-color.otf new file mode 100644 index 0000000000000..f21f9a173f2fe Binary files /dev/null and b/tests/phpunit/data/fonts/gilbert-color.otf differ diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php new file mode 100644 index 0000000000000..a55ab9aaa643f --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -0,0 +1,92 @@ +setAccessible( true ); + + $config = array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => 'my-collection-data.json', + ); + $font_collection = new WP_Font_Collection( $config ); + + $actual = $property->getValue( $font_collection ); + $property->setAccessible( false ); + + $this->assertSame( $config, $actual ); + } + + /** + * @dataProvider data_should_throw_exception + * + * @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 ); + new WP_Font_Collection( $config ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_throw_exception() { + return array( + 'no id' => array( + array( + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => 'my-collection-data.json', + ), + 'Font Collection config ID 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( + array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + ), + 'Font Collection config "src" option is required as a non-empty string.', + ), + + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php new file mode 100644 index 0000000000000..9f843205c3a48 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php @@ -0,0 +1,103 @@ + 'mock', + 'categories' => 'mock', + ); + + return array( + 'body' => json_encode( $mock_collection_data ), + 'response' => array( + 'code' => 200, + ), + ); + } + + /** + * @dataProvider data_should_get_data + * + * @param array $config Font collection config options. + * @param array $expected_data Expected data. + */ + public function test_should_get_data( $config, $expected_data ) { + $collection = new WP_Font_Collection( $config ); + $this->assertSame( $expected_data, $collection->get_data() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_get_data() { + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, '{"this is mock data":true}' ); + + return array( + 'with a file' => array( + 'config' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => $mock_file, + ), + 'expected_data' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'data' => array( 'this is mock data' => true ), + ), + ), + 'with a url' => array( + 'config' => array( + 'id' => 'my-collection-with-url', + 'name' => 'My Collection with URL', + 'description' => 'My collection description', + 'src' => 'https://wordpress.org/fonts/mock-font-collection.json', + ), + 'expected_data' => array( + 'id' => 'my-collection-with-url', + 'name' => 'My Collection with URL', + 'description' => 'My collection description', + 'data' => array( + 'fontFamilies' => 'mock', + 'categories' => 'mock', + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php new file mode 100644 index 0000000000000..c3bd5790956b9 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php @@ -0,0 +1,62 @@ +setAccessible( true ); + + $font_data = array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + ); + $font_family = new WP_Font_Family( $font_data ); + + $actual = $property->getValue( $font_family ); + $property->setAccessible( false ); + + $this->assertSame( $font_data, $actual ); + } + + /** + * @dataProvider data_should_throw_exception + * + * @param mixed $font_data Data to test. + */ + public function test_should_throw_exception( $font_data ) { + $this->expectException( 'Exception' ); + $this->expectExceptionMessage( 'Font family data is missing the slug.' ); + + new WP_Font_Family( $font_data ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_throw_exception() { + return array( + 'no slug' => array( + array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + ), + ), + 'empty array' => array( array() ), + 'boolean instead of array' => array( false ), + 'null instead of array' => array( null ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php new file mode 100644 index 0000000000000..2dd5cd9f42afc --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php @@ -0,0 +1,75 @@ + array(), + 'files_data' => array(), + 'font_filename' => '', + ); + + public static function set_up_before_class() { + parent::set_up_before_class(); + + static::$fonts_dir = WP_Font_Library::get_fonts_dir(); + wp_mkdir_p( static::$fonts_dir ); + } + + public function set_up() { + parent::set_up(); + + $merriweather_tmp_name = wp_tempnam( 'Merriweather-' ); + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $merriweather_tmp_name ); + $this->merriweather = array( + 'font_data' => array( + 'name' => 'Merriweather', + 'slug' => 'merriweather', + 'fontFamily' => 'Merriweather', + 'fontFace' => array( + array( + 'fontFamily' => 'Merriweather', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'merriweather.ttf', + 'type' => 'font/ttf', + 'tmp_name' => $merriweather_tmp_name, + 'error' => 0, + 'size' => 123, + ), + ), + 'font_filename' => path_join( static::$fonts_dir, 'merriweather_normal_400.ttf' ), + ); + } + + public function tear_down() { + // Clean up the /fonts directory. + foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { + @unlink( $file ); + } + + parent::tear_down(); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php new file mode 100644 index 0000000000000..137261de85aa3 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php @@ -0,0 +1,94 @@ +assertSame( $font_data, $font->get_data() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_get_data() { + return array( + 'with one google font face to be downloaded' => array( + 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', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), + ), + ), + ), + 'with one google font face to not be downloaded' => array( + 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', + ), + ), + ), + ), + 'without font faces' => array( + array( + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial', + 'fontFace' => array(), + ), + ), + 'with local files' => array( + array( + 'name' => 'Inter', + 'slug' => 'inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', + ), + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'normal', + 'fontWeight' => '500', + 'uploadedFile' => 'files1', + ), + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php new file mode 100644 index 0000000000000..2c779bff0876f --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php @@ -0,0 +1,67 @@ +assertSame( $expected, $font->get_data_as_json() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_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"}]}', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getPostBySlug.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getPostBySlug.php new file mode 100644 index 0000000000000..01153d275194c --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getPostBySlug.php @@ -0,0 +1,42 @@ + $this->merriweather['font_data']['name'], + 'post_name' => $this->merriweather['font_data']['slug'], + 'post_type' => 'wp_font_family', + 'post_content' => '', + 'post_status' => 'publish', + ); + $post_id = self::factory()->post->create( $post ); + $font = new WP_Font_Family( $this->merriweather['font_data'] ); + + // Test. + $actual = $font->get_post_by_slug(); + $this->assertInstanceOf( WP_Post::class, $actual, 'Font post should exist' ); + $this->assertSame( $post_id, $actual->ID, 'Font post ID should match' ); + } + + public function test_should_return_null_when_post_does_not_exist() { + $font = new WP_Font_Family( $this->merriweather['font_data'] ); + + $this->assertNull( $font->get_post_by_slug() ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php new file mode 100644 index 0000000000000..e493a99334bd8 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php @@ -0,0 +1,78 @@ + 'piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + ), + ), + ); + $font = new WP_Font_Family( $font_data ); + $this->assertTrue( $font->has_font_faces() ); + } + + /** + * @dataProvider data_should_return_false_when_check_fails + * + * @param array $font_data Font family data in theme.json format. + */ + public function test_should_return_false_when_check_fails( $font_data ) { + $font = new WP_Font_Family( $font_data ); + $this->assertFalse( $font->has_font_faces() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_return_false_when_check_fails() { + return array( + 'wrong fontFace key' => array( + array( + 'slug' => 'piazzolla', + 'fontFaces' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + ), + ), + ), + ), + 'without font faces' => array( + array( + 'slug' => 'piazzolla', + ), + ), + 'empty array' => array( + array( + 'slug' => 'piazzolla', + 'fontFace' => array(), + ), + ), + 'null' => array( + array( + 'slug' => 'piazzolla', + 'fontFace' => null, + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php new file mode 100644 index 0000000000000..af096e34df3c6 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php @@ -0,0 +1,562 @@ +install(); + $this->assertEmpty( $this->files_in_dir( static::$fonts_dir ), 'Font directory should be empty' ); + $this->assertInstanceOf( WP_Post::class, $font->get_post_by_slug(), 'Font post should exist after install' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_download_when_no_fontface() { + return array( + 'wrong fontFace key' => array( + array( + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFaces' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + ), + ), + ), + ), + 'without font faces' => array( + array( + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + ), + ), + 'empty array' => array( + array( + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFace' => array(), + ), + ), + 'null' => array( + array( + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFace' => null, + ), + ), + ); + } + + /** + * @dataProvider data_should_download_fontfaces + * + * @param array $font_data Font family data in theme.json format. + * @param array $expected Expected font filename(s). + */ + public function test_should_download_fontfaces_and_create_post( $font_data, array $expected ) { + // Pre-checks to ensure starting conditions. + foreach ( $expected as $font_file ) { + $font_file = path_join( static::$fonts_dir, $font_file ); + $this->assertFileDoesNotExist( $font_file, "Font file [{$font_file}] should not exist in the fonts/ directory after installing" ); + } + $font = new WP_Font_Family( $font_data ); + + // Test. + $font->install(); + foreach ( $expected as $font_file ) { + $font_file = path_join( static::$fonts_dir, $font_file ); + $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" ); + } + $this->assertInstanceOf( WP_Post::class, $font->get_post_by_slug(), 'Font post should exist after install' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_download_fontfaces() { + return array( + '1 font face to download' => 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', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), + ), + ), + 'expected' => array( 'piazzolla_italic_400.ttf' ), + ), + '2 font faces to download' => array( + 'font_data' => array( + 'name' => 'Lato', + 'slug' => 'lato', + 'fontFamily' => 'Lato', + 'fontFace' => array( + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHvxk6XweuBCY.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHvxk6XweuBCY.ttf', + ), + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/lato/v24/S6u8w4BMUTPHjxswWyWrFCbw7A.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/lato/v24/S6u8w4BMUTPHjxswWyWrFCbw7A.ttf', + ), + ), + ), + 'expected' => array( 'lato_normal_400.ttf', 'lato_italic_400.ttf' ), + ), + ); + } + + /** + * @dataProvider data_should_move_local_fontfaces + * + * @param array $font_data Font family data in theme.json format. + * @param array $files_data Files data in $_FILES format. + * @param array $expected Expected font filename(s). + */ + public function test_should_move_local_fontfaces( $font_data, array $files_data, array $expected ) { + // Set up the temporary files. + foreach ( $files_data as $file ) { + if ( 'font/ttf' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); + } elseif ( 'font/woff' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + } elseif ( 'font/woff2' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/DMSans.woff2', $file['tmp_name'] ); + } elseif ( 'application/vnd.ms-opentype' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/gilbert-color.otf', $file['tmp_name'] ); + } + } + + $font = new WP_Font_Family( $font_data ); + $font->install( $files_data ); + + foreach ( $expected as $font_file ) { + $font_file = path_join( static::$fonts_dir, $font_file ); + $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_move_local_fontfaces() { + return array( + // ttf font type. + '1 local font' => array( + 'font_data' => array( + 'name' => 'Inter', + 'slug' => 'inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files0', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'inter1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'inter_italic_900.ttf' ), + ), + '2 local fonts' => array( + 'font_data' => array( + 'name' => 'Lato', + 'slug' => 'lato', + 'fontFamily' => 'Lato', + 'fontFace' => array( + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files1', + ), + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '500', + 'uploadedFile' => 'files2', + ), + ), + ), + 'files_data' => array( + 'files1' => array( + 'name' => 'lato1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + 'files2' => array( + 'name' => 'lato2.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'lato_normal_400.ttf', 'lato_normal_500.ttf' ), + ), + // woff font type. + 'woff local font' => array( + 'font_data' => array( + 'name' => 'Cooper Hewitt', + 'slug' => 'cooper-hewitt', + 'fontFamily' => 'Cooper Hewitt', + 'fontFace' => array( + array( + 'fontFamily' => 'Cooper Hewitt', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files0', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'cooper-hewitt.woff', + 'type' => 'font/woff', + 'tmp_name' => wp_tempnam( 'Cooper-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'cooper-hewitt_italic_900.woff' ), + ), + // woff2 font type. + 'woff2 local font' => array( + 'font_data' => array( + 'name' => 'DM Sans', + 'slug' => 'dm-sans', + 'fontFamily' => 'DM Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'DM Sans', + 'fontStyle' => 'regular', + 'fontWeight' => '500', + 'uploadedFile' => 'files0', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'DMSans.woff2', + 'type' => 'font/woff2', + 'tmp_name' => wp_tempnam( 'DMSans-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'dm-sans_regular_500.woff2' ), + ), + // otf font type. + 'otf local font' => array( + 'font_data' => array( + 'name' => 'Gilbert Color', + 'slug' => 'gilbert-color', + 'fontFamily' => 'Gilbert Color', + 'fontFace' => array( + array( + 'fontFamily' => 'Gilbert Color', + 'fontStyle' => 'regular', + 'fontWeight' => '500', + 'uploadedFile' => 'files0', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'gilbert-color.otf', + 'type' => 'application/vnd.ms-opentype', + 'tmp_name' => wp_tempnam( 'Gilbert-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'gilbert-color_regular_500.otf' ), + ), + ); + } + + /** + * @dataProvider data_should_not_install_duplicate_fontfaces + * + * @param array $font_data Font family data in theme.json format. + * @param array $files_data Files data in $_FILES format. + * @param array $expected Expected font filename(s). + */ + public function test_should_not_install_duplicate_fontfaces( $font_data, array $files_data, array $expected ) { + // Set up the temporary files. + foreach ( $files_data as $file ) { + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); + } + + $font = new WP_Font_Family( $font_data ); + $font->install( $files_data ); + + $this->assertCount( count( $expected ), $this->files_in_dir( static::$fonts_dir ), 'Font directory should contain the same number of files as expected' ); + + foreach ( $expected as $font_file ) { + $font_file = path_join( static::$fonts_dir, $font_file ); + $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_install_duplicate_fontfaces() { + return array( + 'single unique font face' => array( + 'font_data' => array( + 'name' => 'Inter', + 'slug' => 'inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files0', + ), + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files1', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'inter1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + 'files1' => array( + 'name' => 'inter1.woff', + 'type' => 'font/woff', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'inter_italic_900.ttf' ), + ), + 'multiple unique font faces' => array( + 'font_data' => array( + 'name' => 'Lato', + 'slug' => 'lato', + 'fontFamily' => 'Lato', + 'fontFace' => array( + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', + ), + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '500', + 'uploadedFile' => 'files1', + ), + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '500', + 'uploadedFile' => 'files2', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'lato1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + 'files1' => array( + 'name' => 'lato2.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + 'files2' => array( + 'name' => 'lato2.woff', + 'type' => 'font/woff', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'expected' => array( 'lato_normal_400.ttf', 'lato_normal_500.ttf' ), + ), + ); + } + + public function test_should_overwrite_fontface_with_different_extension() { + $font_data_initial = array( + 'name' => 'Inter', + 'slug' => 'inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '500', + 'uploadedFile' => 'files0', + ), + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files1', + ), + ), + ); + $files_data_initial = array( + 'files0' => array( + 'name' => 'inter1.ttf', + 'type' => 'font/woff', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + 'files1' => array( + 'name' => 'inter1.woff', + 'type' => 'font/woff', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + ); + $font_data_overwrite = array( + 'name' => 'Inter', + 'slug' => 'inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '500', + 'uploadedFile' => 'files0', + ), + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files1', + ), + ), + ); + $files_data_overwrite = array( + 'files0' => array( + 'name' => 'inter1.woff', + 'type' => 'font/woff', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + 'files1' => array( + 'name' => 'inter1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + ); + + $expected = array( 'inter_italic_500.woff', 'inter_italic_900.ttf' ); + + // Set up the temporary files. + foreach ( $files_data_initial as $file ) { + if ( 'font/ttf' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); + } elseif ( 'font/woff' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + } + } + foreach ( $files_data_overwrite as $file ) { + if ( 'font/ttf' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); + } elseif ( 'font/woff' === $file['type'] ) { + copy( DIR_TESTDATA . '/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + } + } + + $font = new WP_Font_Family( $font_data_initial ); + $font->install( $files_data_initial ); + + $font = new WP_Font_Family( $font_data_overwrite ); + $font->install( $files_data_overwrite ); + + $this->assertCount( count( $expected ), $this->files_in_dir( static::$fonts_dir ), 'Font directory should contain the same number of files as expected' ); + + foreach ( $expected as $font_file ) { + $font_file = path_join( static::$fonts_dir, $font_file ); + $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" ); + } + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php new file mode 100644 index 0000000000000..b0537e89bc33c --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php @@ -0,0 +1,194 @@ +merriweather['font_data'] ); + + // Test. + $actual = $font->uninstall(); + $this->assertWPError( $actual, 'WP_Error should have been returned' ); + $this->assertSame( + array( 'font_family_not_found' => array( 'The font family could not be found.' ) ), + $actual->errors, + 'WP_Error should have "fonts_must_have_same_slug" error' + ); + } + + /** + * @dataProvider data_should_return_error_when_not_able_to_uninstall + * + * @param string $failure_to_mock The filter name to mock the failure. + */ + public function test_should_return_error_when_not_able_to_uninstall( $failure_to_mock ) { + // Set up the font. + add_filter( $failure_to_mock, '__return_empty_string' ); + $font = new WP_Font_Family( $this->merriweather['font_data'] ); + $font->install( $this->merriweather['files_data'] ); + + // Test. + $actual = $font->uninstall(); + $this->assertWPError( $actual, 'WP_Error should be returned' ); + $this->assertSame( + array( 'font_family_not_deleted' => array( 'The font family could not be deleted.' ) ), + $actual->errors, + 'WP_Error should have "font_family_not_deleted" error' + ); + } + + /** + * Data provider. + * + * @return string[][] + */ + public function data_should_return_error_when_not_able_to_uninstall() { + return array( + 'When delete file fails' => array( 'wp_delete_file' ), + 'when delete post fails' => array( 'pre_delete_post' ), + ); + } + + /** + * @dataProvider data_should_uninstall + * + * @param array $font_data Font family data in theme.json format. + * @param array $files_data Files data in $_FILES format. + */ + public function test_should_uninstall( $font_data, array $files_data ) { + // Set up. + foreach ( $files_data as $file ) { + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); + } + $font = new WP_Font_Family( $font_data ); + $font->install( $files_data ); + + // Pre-checks to ensure the starting point is as expected. + $this->assertInstanceOf( WP_Post::class, $font->get_post_by_slug(), 'Font post should exist' ); + $this->assertNotEmpty( $this->files_in_dir( static::$fonts_dir ), 'Fonts should be installed' ); + + // Uninstall. + $this->assertTrue( $font->uninstall() ); + + // Test the post and font file(s) were uninstalled. + $this->assertNull( $font->get_post_by_slug(), 'Font post should be deleted after uninstall' ); + $this->assertEmpty( $this->files_in_dir( static::$fonts_dir ), 'Fonts should be uninstalled' ); + } + + /** + * @dataProvider data_should_uninstall + * + * @param array $font_data Font family data in theme.json format. + * @param array $files_data Files data in $_FILES format. + * @param array $files_to_uninstall Files to uninstall. + */ + public function test_should_uninstall_only_its_font_family( $font_data, array $files_data, array $files_to_uninstall ) { + // Set up a different font family instance. This font family should not be uninstalled. + $merriweather = new WP_Font_Family( $this->merriweather['font_data'] ); + $merriweather->install( $this->merriweather['files_data'] ); + + // Set up the font family to be uninstalled. + foreach ( $files_data as $file ) { + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); + } + $font = new WP_Font_Family( $font_data ); + $font->install( $files_data ); + + $this->assertTrue( $font->uninstall() ); + + // Check that the files were uninstalled. + foreach ( $files_to_uninstall as $font_file ) { + $font_file = path_join( static::$fonts_dir, $font_file ); + $this->assertFileDoesNotExist( $font_file, "Font file [{$font_file}] should not exists in the uploads/fonts/ directory after uninstalling" ); + } + // Check that the Merriweather file was not uninstalled. + $this->assertFileExists( $this->merriweather['font_filename'], 'The other font family [Merriweather] should not have been uninstalled.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_uninstall() { + return array( + '1 local font' => array( + 'font_data' => array( + 'name' => 'Inter', + 'slug' => 'inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '900', + 'uploadedFile' => 'files0', + ), + ), + ), + 'files_data' => array( + 'files0' => array( + 'name' => 'inter1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Inter-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'files_to_uninstall' => array( 'inter_italic_900.ttf' ), + ), + '2 local fonts' => array( + 'font_data' => array( + 'name' => 'Lato', + 'slug' => 'lato', + 'fontFamily' => 'Lato', + 'fontFace' => array( + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files1', + ), + array( + 'fontFamily' => 'Lato', + 'fontStyle' => 'normal', + 'fontWeight' => '500', + 'uploadedFile' => 'files2', + ), + ), + ), + 'files_data' => array( + 'files1' => array( + 'name' => 'lato1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + 'files2' => array( + 'name' => 'lato2.ttf', + 'type' => 'font/ttf', + 'tmp_name' => wp_tempnam( 'Lato-' ), + 'error' => 0, + 'size' => 123, + ), + ), + 'files_to_uninstall' => array( 'lato_normal_400.ttf', 'lato_normal_500.ttf' ), + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php new file mode 100644 index 0000000000000..7503f4d3571d5 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php @@ -0,0 +1,64 @@ +assertSame( + $expected, + WP_Font_Family_Utils::get_filename_from_font_face( + $slug, + $font_face, + $font_face['src'], + $suffix + ) + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_get_filename() { + 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', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php new file mode 100644 index 0000000000000..17d6488179935 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php @@ -0,0 +1,61 @@ +assertTrue( WP_Font_Family_Utils::has_font_mime_type( $font_file ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_succeed_when_has_mime_type() { + return array( + 'ttf' => array( '/temp/piazzolla_400_italic.ttf' ), + 'otf' => array( '/temp/piazzolla_400_italic.otf' ), + 'woff' => array( '/temp/piazzolla_400_italic.woff' ), + 'woff2' => array( '/temp/piazzolla_400_italic.woff2' ), + ); + } + + /** + * @dataProvider data_should_fail_when_mime_not_supported + * + * @param string $font_file Font file path. + */ + public function test_should_fail_when_mime_not_supported( $font_file ) { + $this->assertFalse( WP_Font_Family_Utils::has_font_mime_type( $font_file ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_fail_when_mime_not_supported() { + return array( + 'exe' => array( '/temp/test.exe' ), + 'md' => array( '/temp/license.md' ), + 'php' => array( '/temp/test.php' ), + 'txt' => array( '/temp/test.txt' ), + 'zip' => array( '/temp/lato.zip' ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php new file mode 100644 index 0000000000000..c1717b5f6c8f3 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php @@ -0,0 +1,300 @@ +assertWPError( $actual, 'WP_Error should have been returned' ); + $this->assertSame( + array( 'fonts_must_have_same_slug' => array( 'Fonts must have the same slug to be merged.' ) ), + $actual->errors, + 'WP_Error should have "fonts_must_have_same_slug" error' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_fail_merge() { + return array( + 'different slugs' => array( + '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', + ), + ); + } + + + /** + * @dataProvider data_should_merge + * + * @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_should_merge( array $font1, array $font2, array $expected_result ) { + $result = WP_Font_Family_Utils::merge_fonts_data( $font1, $font2 ); + $this->assertSame( $expected_result, $result, 'Merged font data should match expected result.' ); + $json_result = wp_json_encode( $result ); + $this->assertStringContainsString( '"fontFace":[', $json_result, 'fontFace data should be enconded as an array and not an object.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_merge() { + return array( + 'with different font faces' => array( + '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', + ), + ), + ), + ), + + 'repeated font faces' => array( + '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', + ), + ), + ), + ), + 'repeated font faces with non consecutive index positions' => array( + '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' => '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' => 'normal', + 'fontWeight' => '600', + 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf', + ), + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '500', + 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf', + ), + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php new file mode 100644 index 0000000000000..5aabaf66dd305 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -0,0 +1,35 @@ + 'my-font-collection', + 'name' => 'My Font Collection', + 'description' => 'Demo about how to a font collection to your WordPress Font Library.', + 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), + ); + + wp_register_font_collection( $my_font_collection_config ); + } + + public function test_should_get_font_collection() { + $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' ); + $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); + } + + public function test_should_get_no_font_collection_if_the_id_is_not_registered() { + $font_collection = WP_Font_Library::get_font_collection( 'not-registered-font-collection' ); + $this->assertWPError( $font_collection ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php new file mode 100644 index 0000000000000..dbe6e0d11209d --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -0,0 +1,45 @@ + 'my-font-collection', + 'name' => 'My Font Collection', + 'description' => 'Demo about how to a font collection to your WordPress Font Library.', + 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), + ); + + $font_library::register_font_collection( $my_font_collection_config ); + } + + public function test_should_get_the_default_font_collection() { + $font_collections = WP_Font_Library::get_font_collections(); + $this->assertArrayHasKey( 'default-font-collection', $font_collections, 'Default Google Fonts collection should be registered' ); + $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['default-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' ); + } + + public function test_should_get_the_right_number_of_collections() { + $font_collections = WP_Font_Library::get_font_collections(); + $this->assertNotEmpty( $font_collections, 'Sould return an array of font collections.' ); + $this->assertCount( 2, $font_collections, 'Should return an array with one font collection.' ); + } + + public function test_should_get_mock_font_collection() { + $font_collections = WP_Font_Library::get_font_collections(); + $this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' ); + $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php new file mode 100644 index 0000000000000..ae2c208bdac9b --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php @@ -0,0 +1,18 @@ +assertStringEndsWith( '/wp-content/fonts', WP_Font_Library::get_fonts_dir() ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php new file mode 100644 index 0000000000000..a1963e5a199d6 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -0,0 +1,65 @@ +get_expected_mime_for_tests_php_version(); + $this->assertSame( $mimes, $expected ); + } + + /** + * Get the expected results for the running PHP version. + * + * @return string[] + */ + private function get_expected_mime_for_tests_php_version() { + // When on less than PHP 7.3. + if ( PHP_VERSION_ID < 70300 ) { + return array( + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => 'application/x-font-ttf', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ); + } + + // When on PHP 7.3. + if ( PHP_VERSION_ID > 70300 && PHP_VERSION_ID < 70400 ) { + return array( + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => 'application/font-sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ); + } + + // When on PHP 7.4 or 8.0. + if ( PHP_VERSION_ID >= 70400 && PHP_VERSION_ID < 80100 ) { + return array( + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => 'font/sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ); + } + + // When on PHP 8.1 or newer. + return array( + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => 'font/sfnt', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php new file mode 100644 index 0000000000000..8475c183099be --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -0,0 +1,77 @@ + 'my-collection', + 'name' => 'My Collection', + 'description' => 'My Collection Description', + 'src' => 'my-collection-data.json', + ); + $collection = WP_Font_Library::register_font_collection( $config ); + $this->assertInstanceOf( 'WP_Font_Collection', $collection ); + } + + public function test_should_return_error_if_id_is_missing() { + $config = array( + 'name' => 'My Collection', + 'description' => 'My Collection Description', + 'src' => 'my-collection-data.json', + ); + $this->expectException( 'Exception' ); + $this->expectExceptionMessage( 'Font Collection config ID is required as a non-empty string.' ); + WP_Font_Library::register_font_collection( $config ); + } + + public function test_should_return_error_if_name_is_missing() { + $config = array( + 'id' => 'my-collection', + '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 ); + } + + 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 ); + } + + public function test_should_return_error_if_id_is_repeated() { + $config1 = array( + 'id' => 'my-collection-1', + 'name' => 'My Collection 1', + 'description' => 'My Collection 1 Description', + 'src' => 'my-collection-1-data.json', + ); + $config2 = array( + 'id' => 'my-collection-1', + 'name' => 'My Collection 2', + 'description' => 'My Collection 2 Description', + 'src' => 'my-collection-2-data.json', + ); + + // Register first collection. + $collection1 = WP_Font_Library::register_font_collection( $config1 ); + $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' ); + + // Try to register a second collection with same id. + $collection2 = WP_Font_Library::register_font_collection( $config2 ); + $this->assertWPError( $collection2, 'Second collection with the same id should fail.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php new file mode 100644 index 0000000000000..a11eaec152e68 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php @@ -0,0 +1,32 @@ + '/abc', + 'basedir' => '/any/path', + 'baseurl' => 'http://example.com/an/arbitrary/url', + 'path' => '/any/path/abc', + 'url' => 'http://example.com/an/arbitrary/url/abc', + ); + $expected = array( + 'subdir' => '/fonts', + 'basedir' => WP_CONTENT_DIR, + 'baseurl' => content_url(), + 'path' => path_join( WP_CONTENT_DIR, 'fonts' ), + 'url' => content_url() . '/fonts', + ); + $this->assertSame( $expected, WP_Font_Library::set_upload_dir( $defaults ) ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php new file mode 100644 index 0000000000000..8df997d7c16fb --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -0,0 +1,311 @@ +factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $admin_id ); + + if ( in_array( $this->getName(), self::$test_methods_to_skip_setup, true ) ) { + return; + } + + // Mock font collection data file. + $mock_file = wp_tempnam( 'one-collection-' ); + file_put_contents( $mock_file, '{"this is mock data":true}' ); + // Mock the wp_remote_request() function. + add_filter( 'pre_http_request', array( $this, 'mock_response' ), 10, 3 ); + + $config_with_file = array( + 'id' => '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( + 'id' => '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( + 'id' => '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( + 'id' => '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 tear_down() { + + // Remove the mock to not affect other tests. + remove_filter( 'pre_http_request', array( $this, 'mock_response' ) ); + + // 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() ); + + // Clean up the /fonts directory. + foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { + @unlink( $file ); + } + + parent::tear_down(); + } + + /** + * @ticket 59166 + * @coversNothing + * + * @param false|array|WP_Error $response A preemptive return value of an HTTP request. + * @param array $parsed_args HTTP request arguments. + * @param string $url The request URL. + * + * @return array Mocked response data. + */ + public function mock_response( $response, $parsed_args, $url ) { + // Check if it's the URL you want to mock. + if ( 'https://wordpress.org/fonts/mock-font-collection.json' !== $url ) { + // For any other URL, return false which ensures the request is made as usual (or you can return other mock data). + return false; + } + + // Mock the response body. + $mock_collection_data = array( + 'fontFamilies' => 'mock', + 'categories' => 'mock', + ); + + return array( + 'body' => json_encode( $mock_collection_data ), + 'response' => array( + 'code' => 200, + ), + ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/font-collections/(?P[\/\w-]+)', + $routes, + "Font collections route doesn't exist." + ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'The REST server does not have the GET method initialized for font collections.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'The REST server does not have the GET method initialized for a specific font collection.' ); + $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'The REST server does not have the font collections path initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P[\/\w-]+)'], 'The REST server does not have the path initialized for a specific font collection.' ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_items + */ + public function test_get_items() { + // 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( + 'id' => '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( 'id', $data[0], 'The response data does not have the key with the collection ID.' ); + $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_items + */ + public function test_get_items_with_no_collection_registered() { + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( array(), $response->get_data() ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_item + */ + public function test_get_item() { + $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.' ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_item + */ + public function test_get_item_should_return_font_colllection_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.' ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_item + */ + 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() ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_item + */ + 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() ); + } + + /** + * @ticket 59166 + * @covers WP_REST_Font_Collections_Controller::get_item + */ + 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() ); + } + + public function test_context_param() { + } + + /** + * @ticket 59166 + * @coversNothing + */ + public function test_create_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to create font collection.", + WP_REST_Template_Autosaves_Controller::class + ) + ); + } + + /** + * @ticket 59166 + * @coversNothing + */ + public function test_update_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to update font collection.", + WP_REST_Font_Collections_Controller::class + ) + ); + } + + /** + * @ticket 59166 + * @coversNothing + */ + public function test_delete_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to delete font collection.", + WP_REST_Font_Collections_Controller::class + ) + ); + } + + /** + * @covers WP_REST_Font_Collections_Controller::prepare_item_for_response + * @ticket 56922 + */ + public function test_prepare_item() { + $font_collection_id = 'collection-with-url'; + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/' . $font_collection_id ); + + /** @var WP_Font_Collection $font_collection */ + $font_collection = WP_Font_Library::get_font_collection( $font_collection_id ); + + $font_collection_data = $font_collection->get_data(); + $controller = new WP_REST_Font_Collections_Controller(); + $response_font_collection_data = $controller->prepare_item_for_response( $font_collection_data, $request ); + $this->assertIsArray( $font_collection_data, 'Font collection data should be an array.' ); + $this->assertSame( $font_collection_data['id'], $response_font_collection_data['id'], 'Font collection ID should remain consistent in the response.' ); + $this->assertSame( $font_collection_data['name'], $response_font_collection_data['name'], 'Font collection name should be consistent in the response.' ); + $this->assertSame( $font_collection_data['description'], $response_font_collection_data['description'], 'Font collection description should be consistent in the response.' ); + $this->assertIsArray( $response_font_collection_data['data'], 'Response font collection data should be an array.' ); + $this->assertSame( $font_collection_data['data']['categories'], $response_font_collection_data['data']['categories'], 'Font collection categories should be consistent in the response.' ); + $this->assertArrayNotHasKey( 'fontFamilies', $response_font_collection_data['data'], 'Response font collection data should not contain the "fontFamilies" key.' ); + $this->assertSame( $font_collection_data['data']['fontFamilies'], $response_font_collection_data['data']['font_families'], 'Font families should be consistent in the response.' ); + } + + public function test_get_item_schema() { + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php new file mode 100644 index 0000000000000..97ff9c7e96911 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php @@ -0,0 +1,126 @@ + '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( + 'id' => '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( + 'id' => '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( + 'id' => '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/fonts/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/fonts/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/fonts/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/fonts/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/fonts/collections/collection-with-non-existing-url' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 500, $response->get_status() ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php new file mode 100644 index 0000000000000..79742e4d7d8d5 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php @@ -0,0 +1,44 @@ +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( + 'id' => '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/fonts/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( 'id', $data[0], 'The response data does not have the key with the collection ID.' ); + $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php new file mode 100644 index 0000000000000..e10cac18db40b --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php @@ -0,0 +1,43 @@ +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 ); + } + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/createItem.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/createItem.php new file mode 100644 index 0000000000000..24fc715008449 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/createItem.php @@ -0,0 +1,308 @@ +set_param( 'slug', $font_family['slug'] ); + $create_item_request->set_param( 'fontFamily', $font_family['fontFamily'] ); + $create_item_request->set_param( 'name', $font_family['name'] ); + if ( ! empty( $font_family['fontFace'] ) ) { + $create_item_request->set_param( 'fontFace', json_encode( $font_family['fontFace'] ) ); + } + $create_item_request->set_file_params( $files ); + $response = rest_get_server()->dispatch( $create_item_request ); + $installed_font = $response->get_data(); + $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); + + if ( isset( $installed_font['fontFace'] ) || isset( $expected_response['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_response['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_response['fontFace'][ $face_index ]['src'] ); + unset( $installed_font['fontFace'][ $face_index ]['uploadedFile'] ); + } + } + + // Compares if the rest of the data is the same. + $this->assertEquals( $expected_response, $installed_font, 'The endpoint answer is not as expected.' ); + } + + /** + * Data provider for test_install_fonts + */ + public function data_create_item() { + $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $temp_file_path1 ); + + return array( + + 'google_fonts_to_download' => array( + 'font_family' => 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( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + ), + ), + ), + ), + + 'google_fonts_to_use_as_is' => array( + 'font_family' => 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( + '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', + ), + ), + ), + ), + + 'fonts_without_font_faces' => array( + 'font_family' => array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', + ), + 'files' => array(), + 'expected_response' => array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', + ), + ), + + 'fonts_with_local_fonts_assets' => array( + 'font_family' => 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, + ), + ), + 'expected_response' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + ), + ), + ), + ), + ); + } + + /** + * Tests failure when fontFaces has improper inputs + * + * @dataProvider data_create_item_with_improper_inputs + * + * @param array $font_family Font families to install in theme.json format. + * @param array $files Font files to install. + */ + public function test_create_item_with_improper_inputs( $font_family, $files = array() ) { + $create_item_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + + if ( isset( $font_family['slug'] ) ) { + $create_item_request->set_param( 'slug', $font_family['slug'] ); + } + if ( isset( $font_family['fontFamily'] ) ) { + $create_item_request->set_param( 'fontFamily', $font_family['fontFamily'] ); + } + if ( isset( $font_family['name'] ) ) { + $create_item_request->set_param( 'name', $font_family['name'] ); + } + if ( isset( $font_family['fontFace'] ) ) { + $create_item_request->set_param( 'fontFace', json_encode( $font_family['fontFace'] ) ); + } + $create_item_request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $create_item_request ); + $this->assertSame( 400, $response->get_status() ); + } + + /** + * Data provider for test_install_with_improper_inputs + */ + public function data_create_item_with_improper_inputs() { + + $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $temp_file_path1 ); + + return array( + 'empty array' => array( + 'font_family' => array(), + ), + + 'without slug' => array( + 'font_family' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + ), + ), + + 'with improper font face property' => array( + 'font_family' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => 'This is not an array', + ), + ), + + 'with empty font face property' => array( + 'font_family' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array(), + ), + ), + + 'fontface referencing uploaded file without uploaded files' => array( + 'font_family' => 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' => 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' => 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/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/deleteItem.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/deleteItem.php new file mode 100644 index 0000000000000..8f6e34f656a2f --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/deleteItem.php @@ -0,0 +1,66 @@ + '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', + ), + ), + ), + ); + + foreach ( $mock_families as $font_family ) { + + $create_item_request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + + $create_item_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $create_item_request->set_param( 'slug', $font_family['slug'] ); + $create_item_request->set_param( 'fontFamily', $font_family['fontFamily'] ); + $create_item_request->set_param( 'name', $font_family['name'] ); + if ( ! empty( $font_family['fontFace'] ) ) { + $create_item_request->set_param( 'fontFace', json_encode( $font_family['fontFace'] ) ); + } + rest_get_server()->dispatch( $create_item_request ); + } + } + + public function test_delete_item() { + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/piazzolla' ); + $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/pizza' ); + $response = rest_get_server()->dispatch( $uninstall_request ); + $this->assertSame( 404, $response->get_status(), 'The response status is not 404.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItem.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItem.php new file mode 100644 index 0000000000000..c28ec85d35b7b --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItem.php @@ -0,0 +1,96 @@ + 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://example.com/fonts/example1.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://example.com/fonts/example2.ttf', + ), + ), + ), + ); + + foreach ( $mock_families as $font_family ) { + + $create_item_request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + + $create_item_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $create_item_request->set_param( 'slug', $font_family['slug'] ); + $create_item_request->set_param( 'fontFamily', $font_family['fontFamily'] ); + $create_item_request->set_param( 'name', $font_family['name'] ); + if ( ! empty( $font_family['fontFace'] ) ) { + $create_item_request->set_param( 'fontFace', json_encode( $font_family['fontFace'] ) ); + } + rest_get_server()->dispatch( $create_item_request ); + } + } + + public function tear_down() { + parent::tear_down(); + + // Delete mock fonts after tests. + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/montserrat' ); + rest_get_server()->dispatch( $uninstall_request ); + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/piazzolla' ); + rest_get_server()->dispatch( $uninstall_request ); + } + + public function test_get_item() { + $get_items_request = new WP_REST_Request( 'GET', '/wp/v2/font-families/piazzolla' ); + $response = rest_get_server()->dispatch( $get_items_request ); + $expected_response = array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://example.com/fonts/example1.ttf', + ), + ), + ); + $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); + $this->assertEquals( $expected_response, $response->get_data(), 'The response data is not expected.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItems.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItems.php new file mode 100644 index 0000000000000..9eb68e36c63d3 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItems.php @@ -0,0 +1,112 @@ + 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://example.com/fonts/example1.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://example.com/fonts/example2.ttf', + ), + ), + ), + ); + + foreach ( $mock_families as $font_family ) { + + $create_item_request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + + $create_item_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $create_item_request->set_param( 'slug', $font_family['slug'] ); + $create_item_request->set_param( 'fontFamily', $font_family['fontFamily'] ); + $create_item_request->set_param( 'name', $font_family['name'] ); + if ( ! empty( $font_family['fontFace'] ) ) { + $create_item_request->set_param( 'fontFace', json_encode( $font_family['fontFace'] ) ); + } + rest_get_server()->dispatch( $create_item_request ); + } + } + + public function tear_down() { + parent::tear_down(); + + // Delete mock fonts after tests. + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/montserrat' ); + rest_get_server()->dispatch( $uninstall_request ); + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/piazzolla' ); + rest_get_server()->dispatch( $uninstall_request ); + } + + public function test_get_items() { + $get_items_request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + $response = rest_get_server()->dispatch( $get_items_request ); + $expected_response = array( + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://example.com/fonts/example2.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://example.com/fonts/example1.ttf', + ), + ), + ), + ); + $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); + + $this->assertEquals( $expected_response, $response->get_data(), 'The response data is not expected.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/registerRoutes.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/registerRoutes.php new file mode 100644 index 0000000000000..3c033eb73b55d --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/registerRoutes.php @@ -0,0 +1,25 @@ +get_routes(); + $this->assertArrayHasKey( '/wp/v2/font-families', $routes, 'Rest server has not the fonts path intialized.' ); + $this->assertCount( 2, $routes['/wp/v2/font-families'], 'Rest server has not the 2 fonts paths initialized.' ); + $this->assertCount( 2, $routes['/wp/v2/font-families/(?P[\/\w-]+)'], 'Rest server has not the 2 fonts paths initialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-families'][0]['methods'], 'Rest server has not the GET method for fonts intialized.' ); + $this->assertArrayHasKey( 'POST', $routes['/wp/v2/font-families'][1]['methods'], 'Rest server has not the POST method for fonts intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-families/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for fonts intialized.' ); + $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/font-families/(?P[\/\w-]+)'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index dfd98877d8ed3..feaae40d70363 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -133,6 +133,10 @@ public function test_expected_routes_in_schema() { '/wp/v2/users/(?P(?:[\\d]+|me))/application-passwords/(?P[\\w\\-]+)', '/wp/v2/comments', '/wp/v2/comments/(?P[\\d]+)', + '/wp/v2/font-families', + '/wp/v2/font-families/(?P[\/\w-]+)', + '/wp/v2/font-collections', + '/wp/v2/font-collections/(?P[\/\w-]+)', '/wp/v2/global-styles/(?P[\/\w-]+)', '/wp/v2/global-styles/(?P[\d]+)/revisions', '/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)/variations', diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 8341830452a1f..344f73c1ed773 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -18,13 +18,7 @@ mockedApiResponse.Schema = { "wp-site-health/v1", "wp-block-editor/v1" ], - "authentication": { - "application-passwords": { - "endpoints": { - "authorization": "http://example.org/wp-admin/authorize-application.php" - } - } - }, + "authentication": [], "routes": { "/": { "namespace": "", @@ -11469,6 +11463,113 @@ mockedApiResponse.Schema = { } ] } + }, + "/wp/v2/font-families": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + }, + { + "methods": [ + "POST" + ], + "args": { + "slug": { + "type": "string", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "fontFamily": { + "type": "string", + "required": true + }, + "fontFace": { + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/font-families" + } + ] + } + }, + "/wp/v2/font-families/(?P[\\/\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + }, + { + "methods": [ + "DELETE" + ], + "args": [] + } + ] + }, + "/wp/v2/font-collections": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/font-collections" + } + ] + } + }, + "/wp/v2/font-collections/(?P[\\/\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "string", + "required": true + } + } + } + ] } }, "site_logo": 0,