From 9a008143c0c14c6527a71c40eedc9596378e31fe Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 15:14:39 -0300 Subject: [PATCH 001/105] add main backend files --- .../font-library/class-wp-font-collection.php | 112 ++++ .../class-wp-font-family-utils.php | 92 +++ .../font-library/class-wp-font-family.php | 619 ++++++++++++++++++ .../font-library/class-wp-font-library.php | 135 ++++ .../class-wp-rest-font-library-controller.php | 398 +++++++++++ .../fonts/font-library/font-library.php | 74 +++ 6 files changed, 1430 insertions(+) create mode 100644 src/wp-includes/fonts/font-library/class-wp-font-collection.php create mode 100644 src/wp-includes/fonts/font-library/class-wp-font-family-utils.php create mode 100644 src/wp-includes/fonts/font-library/class-wp-font-family.php create mode 100644 src/wp-includes/fonts/font-library/class-wp-font-library.php create mode 100644 src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php create mode 100644 src/wp-includes/fonts/font-library/font-library.php diff --git a/src/wp-includes/fonts/font-library/class-wp-font-collection.php b/src/wp-includes/fonts/font-library/class-wp-font-collection.php new file mode 100644 index 0000000000000..5b3702ae865d1 --- /dev/null +++ b/src/wp-includes/fonts/font-library/class-wp-font-collection.php @@ -0,0 +1,112 @@ +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 ( false !== strpos( $this->config['src'], 'http' ) && false !== strpos( $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.', 'gutenberg' ) ); + } + + $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.', 'gutenberg' ) ); + } + + $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.', 'gutenberg' ) ); + } + // 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.', 'gutenberg' ) ); + } + $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.', 'gutenberg' ) ); + } + } + + $collection_data = $this->get_config(); + $collection_data['data'] = $data; + unset( $collection_data['src'] ); + return $collection_data; + } +} diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php b/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php new file mode 100644 index 0000000000000..200cfef22289f --- /dev/null +++ b/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php @@ -0,0 +1,92 @@ +data = $font_data; + } + + /** + * Gets the font family data. + * + * @since 6.4.0 + * + * @return array An array in fontFamily theme.json format. + */ + public function get_data() { + return $this->data; + } + + /** + * Gets the font family data. + * + * @since 6.4.0 + * + * @return string fontFamily in theme.json format as stringified JSON. + */ + public function get_data_as_json() { + return wp_json_encode( $this->get_data() ); + } + + /** + * Checks whether the font family has font faces defined. + * + * @since 6.4.0 + * + * @return bool True if the font family has font faces defined, false otherwise. + */ + public function has_font_faces() { + return ! empty( $this->data['fontFace'] ) && is_array( $this->data['fontFace'] ); + } + + /** + * Removes font family assets. + * + * @since 6.4.0 + * + * @return bool True if assets were removed, false otherwise. + */ + private function remove_font_family_assets() { + if ( $this->has_font_faces() ) { + foreach ( $this->data['fontFace'] as $font_face ) { + $were_assets_removed = $this->delete_font_face_assets( $font_face ); + if ( false === $were_assets_removed ) { + return false; + } + } + } + return true; + } + + /** + * Removes a font family from the database and deletes its assets. + * + * @since 6.4.0 + * + * @return bool|WP_Error True if the font family was uninstalled, WP_Error otherwise. + */ + public function uninstall() { + $post = $this->get_data_from_post(); + if ( null === $post ) { + return new WP_Error( + 'font_family_not_found', + __( 'The font family could not be found.', 'gutenberg' ) + ); + } + + if ( + ! $this->remove_font_family_assets() || + ! wp_delete_post( $post->ID, true ) + ) { + return new WP_Error( + 'font_family_not_deleted', + __( 'The font family could not be deleted.', 'gutenberg' ) + ); + } + + return true; + } + + /** + * Deletes a specified font asset file from the fonts directory. + * + * @since 6.4.0 + * + * @param string $src The path of the font asset file to delete. + * @return bool Whether the file was deleted. + */ + private static function delete_asset( $src ) { + $filename = basename( $src ); + $file_path = path_join( WP_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::ALLOWED_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_Gutenberg( $fonts_json ); + $theme_data = $theme_json->get_data(); + $sanitized_font = ! empty( $theme_data['settings']['typography']['fontFamilies'] ) + ? $theme_data['settings']['typography']['fontFamilies'][0] + : array(); + $this->data = $sanitized_font; + return $this->data; + } + + /** + * Downloads font face assets. + * + * Downloads the font face asset(s) associated with a font face. It works with + * both single source URLs and arrays of multiple source URLs. + * + * @since 6.4.0 + * + * @param array $font_face The font face array containing the 'src' attribute + * with the source URL(s) of the assets. + * @return array The modified font face array with the new source URL(s) to + * the downloaded assets. + */ + private function download_font_face_assets( $font_face ) { + $new_font_face = $font_face; + $sources = (array) $font_face['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 the post for a font family. + * + * @since 6.4.0 + * + * @return WP_Post|null The post for this font family object or + * null if the post does not exist. + */ + public function get_font_post() { + $args = array( + 'post_type' => 'wp_font_family', + 'post_name' => $this->data['slug'], + 'name' => $this->data['slug'], + 'posts_per_page' => 1, + ); + + $posts_query = new WP_Query( $args ); + + if ( $posts_query->have_posts() ) { + return $posts_query->posts[0]; + } + + return null; + } + + /** + * Gets the data for this object from the database and + * sets it to the data property. + * + * @since 6.4.0 + * + * @return WP_Post|null The post for this font family object or + * null if the post does not exist. + */ + private function get_data_from_post() { + $post = $this->get_font_post(); + if ( $post ) { + $this->data = json_decode( $post->post_content, true ); + return $post; + } + + return null; + } + + /** + * Creates a post for a font family. + * + * @since 6.4.0 + * + * @return int|WP_Error Post ID if the post was created, WP_Error otherwise. + */ + private function create_font_post() { + $post = array( + 'post_title' => $this->data['name'], + 'post_name' => $this->data['slug'], + 'post_type' => 'wp_font_family', + 'post_content' => $this->get_data_as_json(), + 'post_status' => 'publish', + ); + + $post_id = wp_insert_post( $post ); + if ( 0 === $post_id || is_wp_error( $post_id ) ) { + return new WP_Error( + 'font_post_creation_failed', + __( 'Font post creation failed', 'gutenberg' ) + ); + } + + return $post_id; + } + + /** + * 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 ); + $new_data = WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data ); + if ( isset( $post_font_data['fontFace'] ) && ! empty( $post_font_data['fontFace'] ) ) { + $intersecting = $this->get_intersecting_font_faces( $post_font_data['fontFace'], $new_data['fontFace'] ); + } + + if ( isset( $intersecting ) && ! 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', 'gutenberg' ) + ); + } + + 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_font_post(); + if ( $post ) { + return $this->update_font_post( $post ); + } + + return $this->create_font_post(); + } + + /** + * Installs the font family into the library. + * + * @since 6.4.0 + * + * @param array $files Optional. An array of files to be installed. Default null. + * @return array|WP_Error An array of font family data on success, WP_Error otherwise. + */ + public function install( $files = null ) { + add_filter( 'upload_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.', 'gutenberg' ) + ); + } + + $post_id = $this->create_or_update_font_post(); + + if ( is_wp_error( $post_id ) ) { + return $post_id; + } + + return $this->get_data(); + } +} diff --git a/src/wp-includes/fonts/font-library/class-wp-font-library.php b/src/wp-includes/fonts/font-library/class-wp-font-library.php new file mode 100644 index 0000000000000..d488e330486a7 --- /dev/null +++ b/src/wp-includes/fonts/font-library/class-wp-font-library.php @@ -0,0 +1,135 @@ += 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; + + const ALLOWED_FONT_MIME_TYPES = array( + 'otf' => 'font/otf', + 'ttf' => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : self::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', + ); + + /** + * Font collections. + * + * @since 6.4.0 + * + * @var array + */ + private static $collections = array(); + + /** + * Register 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::ALLOWED_FONT_MIME_TYPES ); + } +} diff --git a/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php b/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php new file mode 100644 index 0000000000000..dcf94d93012a2 --- /dev/null +++ b/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php @@ -0,0 +1,398 @@ +rest_base = 'fonts'; + $this->namespace = 'wp/v2'; + } + + /** + * Registers the routes for the objects of the controller. + * + * @since 6.4.0 + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'install_fonts' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'args' => array( + 'fontFamilies' => array( + 'required' => true, + 'type' => 'string', + 'validate_callback' => array( $this, 'validate_install_font_families' ), + ), + ), + ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'uninstall_fonts' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'args' => $this->uninstall_schema(), + ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/collections', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_font_collections' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/collections' . '/(?P[\/\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_font_collection' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + ) + ); + } + + /** + * 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_font_collection( $request ) { + $id = $request->get_param( '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; + } + return new WP_REST_Response( $collection_with_data ); + } + + /** + * Gets the font collections available. + * + * @since 6.4.0 + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_font_collections() { + $collections = array(); + foreach ( WP_Font_Library::get_font_collections() as $collection ) { + $collections[] = $collection->get_config(); + } + + return new WP_REST_Response( $collections, 200 ); + } + + /** + * Returns validation errors in font families data for installation. + * + * @since 6.4.0 + * + * @param array[] $font_families Font families to install. + * @param array $files Files to install. + * @return array $error_messages Array of error messages. + */ + private function get_validation_errors( $font_families, $files ) { + $error_messages = array(); + + if ( ! is_array( $font_families ) ) { + $error_messages[] = __( 'fontFamilies should be an array of font families.', 'gutenberg' ); + return $error_messages; + } + + // Checks if there is at least one font family. + if ( count( $font_families ) < 1 ) { + $error_messages[] = __( 'fontFamilies should have at least one font family definition.', 'gutenberg' ); + return $error_messages; + } + + for ( $family_index = 0; $family_index < count( $font_families ); $family_index++ ) { + $font_family = $font_families[ $family_index ]; + + if ( + ! isset( $font_family['slug'] ) || + ! isset( $font_family['name'] ) || + ! isset( $font_family['fontFamily'] ) + ) { + $error_messages[] = sprintf( + // translators: 1: font family index. + __( 'Font family [%s] should have slug, name and fontFamily properties defined.', 'gutenberg' ), + $family_index + ); + } + + if ( isset( $font_family['fontFace'] ) ) { + if ( ! is_array( $font_family['fontFace'] ) ) { + $error_messages[] = sprintf( + // translators: 1: font family index. + __( 'Font family [%s] should have fontFace property defined as an array.', 'gutenberg' ), + $family_index + ); + continue; + } + + if ( count( $font_family['fontFace'] ) < 1 ) { + $error_messages[] = sprintf( + // translators: 1: font family index. + __( 'Font family [%s] should have at least one font face definition.', 'gutenberg' ), + $family_index + ); + } + + if ( ! empty( $font_family['fontFace'] ) ) { + for ( $face_index = 0; $face_index < count( $font_family['fontFace'] ); $face_index++ ) { + + $font_face = $font_family['fontFace'][ $face_index ]; + if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { + $error_messages[] = sprintf( + // translators: 1: font family index, 2: font face index. + __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), + $family_index, + $face_index + ); + } + + if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { + $error_messages[] = sprintf( + // translators: 1: font family index, 2: font face index. + __( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), + $family_index, + $face_index + ); + } + + if ( isset( $font_face['uploadedFile'] ) ) { + if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { + $error_messages[] = sprintf( + // translators: 1: font family index, 2: font face index. + __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.', 'gutenberg' ), + $family_index, + $face_index + ); + } + } + } + } + } + } + + return $error_messages; + } + + /** + * Validate input for the install endpoint. + * + * @since 6.4.0 + * + * @param string $param The font families to install. + * @param WP_REST_Request $request The request object. + * @return true|WP_Error True if the parameter is valid, WP_Error otherwise. + */ + public function validate_install_font_families( $param, $request ) { + $font_families = json_decode( $param, true ); + $files = $request->get_file_params(); + $error_messages = $this->get_validation_errors( $font_families, $files ); + + if ( empty( $error_messages ) ) { + return true; + } + + return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) ); + } + + /** + * Gets the schema for the uninstall endpoint. + * + * @since 6.4.0 + * + * @return array Schema array. + */ + public function uninstall_schema() { + return array( + 'fontFamilies' => array( + 'type' => 'array', + 'description' => __( 'The font families to install.', 'gutenberg' ), + 'required' => true, + 'minItems' => 1, + 'items' => array( + 'required' => true, + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The font family slug.', 'gutenberg' ), + 'required' => true, + ), + ), + ), + ), + ); + } + + /** + * Removes font families from the 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 uninstall_fonts( $request ) { + $fonts_param = $request->get_param( 'fontFamilies' ); + + foreach ( $fonts_param as $font_data ) { + $font = new WP_Font_Family( $font_data ); + $result = $font->uninstall(); + + // If there was an error uninstalling the font, return the error. + if ( is_wp_error( $result ) ) { + return $result; + } + } + + return new WP_REST_Response( __( 'Font family uninstalled successfully.', 'gutenberg' ), 200 ); + } + + /** + * Checks whether the user has permissions to update the 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.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + // The update endpoints requires write access to the temp and the fonts directories. + $temp_dir = get_temp_dir(); + $upload_dir = wp_upload_dir()['basedir']; + if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { + return new WP_Error( + 'rest_cannot_write_fonts_folder', + __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ), + array( + 'status' => 500, + ) + ); + } + + return true; + } + + /** + * Installs new fonts. + * + * Takes a request containing new fonts to install, downloads their assets, and adds them + * to the 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 install_fonts( $request ) { + // Get new fonts to install. + $fonts_param = $request->get_param( 'fontFamilies' ); + + /* + * As this is receiving form data, the font families are encoded as a string. + * The form data is used because local fonts need to use that format to + * attach the files in the request. + */ + $fonts_to_install = json_decode( $fonts_param, true ); + + if ( empty( $fonts_to_install ) ) { + return new WP_Error( + 'no_fonts_to_install', + __( 'No fonts to install', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Get uploaded files (used when installing local fonts). + $files = $request->get_file_params(); + + // Iterates the fonts data received and creates a new WP_Font_Family object for each one. + $fonts_installed = array(); + foreach ( $fonts_to_install as $font_data ) { + $font = new WP_Font_Family( $font_data ); + $font->install( $files ); + $fonts_installed[] = $font; + } + + if ( empty( $fonts_installed ) ) { + return new WP_Error( + 'error_installing_fonts', + __( 'Error installing fonts. No font was installed.', 'gutenberg' ), + array( 'status' => 500 ) + ); + } + + $response = array(); + foreach ( $fonts_installed as $font ) { + $response[] = $font->get_data(); + } + + return new WP_REST_Response( $response ); + } +} diff --git a/src/wp-includes/fonts/font-library/font-library.php b/src/wp-includes/fonts/font-library/font-library.php new file mode 100644 index 0000000000000..701694155c48b --- /dev/null +++ b/src/wp-includes/fonts/font-library/font-library.php @@ -0,0 +1,74 @@ + false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'label' => 'Font Library', + 'show_in_rest' => true, + ); + register_post_type( 'wp_font_family', $args ); + + // @core-merge: This code will go into Core's `create_initial_rest_routes()`. + $font_library_controller = new WP_REST_Font_Library_Controller(); + $font_library_controller->register_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); + + +if ( ! function_exists( 'wp_register_font_collection' ) ) { + /** + * 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 ); + } +} + +add_action( + 'enqueue_block_editor_assets', + function () { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalFontLibrary = true', 'before' ); + } +); + +$default_font_collection = array( + 'id' => 'default-font-collection', + 'name' => 'Google Fonts', + 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), + 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', +); + +wp_register_font_collection( $default_font_collection ); From 6f9aefcf964503e0852f189ccef00518d8f5915f Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 15:23:16 -0300 Subject: [PATCH 002/105] add comment about CDN link --- .../fonts/font-library/font-library.php | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/src/wp-includes/fonts/font-library/font-library.php b/src/wp-includes/fonts/font-library/font-library.php index 701694155c48b..c76bce4462c46 100644 --- a/src/wp-includes/fonts/font-library/font-library.php +++ b/src/wp-includes/fonts/font-library/font-library.php @@ -1,3 +1,4 @@ + false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'label' => 'Font Library', - 'show_in_rest' => true, - ); - register_post_type( 'wp_font_family', $args ); - - // @core-merge: This code will go into Core's `create_initial_rest_routes()`. - $font_library_controller = new WP_REST_Font_Library_Controller(); - $font_library_controller->register_routes(); -} - -add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); - - if ( ! function_exists( 'wp_register_font_collection' ) ) { /** * Registers a new Font Collection in the Font Library. @@ -67,8 +40,9 @@ function () { $default_font_collection = array( 'id' => 'default-font-collection', 'name' => 'Google Fonts', - 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), - 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', + 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'default' ), + /* This will be changed to a wporg CDN URL, see: https://meta.trac.wordpress.org/ticket/7282#ticket */ + 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', // ); wp_register_font_collection( $default_font_collection ); From d78d99bb0f0d3fe39e9c08c6ffb22b13b56f28d1 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 15:27:54 -0300 Subject: [PATCH 003/105] load main files --- src/wp-includes/post.php | 10 ++++++++++ src/wp-includes/rest-api.php | 4 ++++ src/wp-settings.php | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 696e74ed2c9ef..702136720aca9 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -558,6 +558,16 @@ function create_initial_post_types() { ) ); + register_post_type( + 'wp_font_family', + array( + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'label' => __( 'Font Library' ), + 'show_in_rest' => true, + ) + ); + register_post_status( 'publish', array( diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a8a2aac177288..c8c63be7e68a4 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -385,6 +385,10 @@ function create_initial_rest_routes() { // Navigation Fallback. $controller = new WP_REST_Navigation_Fallback_Controller(); $controller->register_routes(); + + // Font Library. + $controller = new WP_REST_Font_Library_Controller(); + $controller->register_routes(); } /** diff --git a/src/wp-settings.php b/src/wp-settings.php index 528f335cb7c2a..77deabd71c424 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -364,6 +364,12 @@ 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/font-library/class-wp-font-collection.php'; +require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-library.php'; +require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family-utils.php'; +require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family.php'; +require ABSPATH . WPINC . '/fonts/font-library/class-wp-rest-font-library-controller.php'; +require ABSPATH . WPINC . '/fonts/font-library/font-library.php'; $GLOBALS['wp_embed'] = new WP_Embed(); From 1d741441f0334bd9e99d42f67876da75ec28b9b4 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 15:33:56 -0300 Subject: [PATCH 004/105] add routes from font library to routes test --- tests/phpunit/tests/rest-api/rest-schema-setup.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index dfd98877d8ed3..c33e9d5190c11 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -188,6 +188,10 @@ public function test_expected_routes_in_schema() { '/wp-site-health/v1/directory-sizes', '/wp/v2/wp_pattern_category', '/wp/v2/wp_pattern_category/(?P[\d]+)', + '/wp/v2/fonts', + '/wp/v2/fonts/collections', + '/wp/v2/fonts/collections/(?P[\/\w-]+)', + '' ); $this->assertSameSets( $expected_routes, $routes ); From 42eeb481fcd54ce3fd17f9294c19f971b413f40a Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 15:43:54 -0300 Subject: [PATCH 005/105] adding PHP unit tests for font library --- .../wpFontCollection/__construct.php | 92 +++ .../font-library/wpFontCollection/getData.php | 103 ++++ .../font-library/wpFontFamily/__construct.php | 62 ++ .../fonts/font-library/wpFontFamily/base.php | 75 +++ .../font-library/wpFontFamily/getData.php | 94 +++ .../wpFontFamily/getDataAsJson.php | 67 +++ .../font-library/wpFontFamily/getFontPost.php | 42 ++ .../wpFontFamily/hasFontFaces.php | 78 +++ .../font-library/wpFontFamily/install.php | 534 ++++++++++++++++++ .../font-library/wpFontFamily/uninstall.php | 194 +++++++ .../getFilenameFromFontFace.php | 64 +++ .../wpFontFamilyUtils/hasFontMimeType.php | 61 ++ .../wpFontFamilyUtils/mergeFontsData.php | 300 ++++++++++ .../wpFontLibrary/getFontCollection.php | 35 ++ .../wpFontLibrary/getFontCollections.php | 45 ++ .../wpFontLibrary/getFontsDir.php | 18 + .../wpFontLibrary/registerFontCollection.php | 77 +++ .../wpFontLibrary/setUploadDir.php | 30 + .../wpRestFontLibraryController/base.php | 34 ++ .../getFontCollection.php | 126 +++++ .../getFontCollections.php | 45 ++ .../installFonts.php | 420 ++++++++++++++ .../registerRoutes.php | 28 + .../uninstallFonts.php | 97 ++++ 24 files changed, 2721 insertions(+) create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php 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..5c2b7b5c02793 --- /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..4d0b2eb92b595 --- /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://localhost/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..3a1e387c3651b --- /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..3650ac7dab997 --- /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__ . '/../../../data/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..2de88ad3aae37 --- /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..b18b2d0b4f4b3 --- /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/getFontPost.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php new file mode 100644 index 0000000000000..eb42cc3ee0898 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.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 = wp_insert_post( $post ); + $font = new WP_Font_Family( $this->merriweather['font_data'] ); + + // Test. + $actual = $font->get_font_post(); + $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_font_post() ); + } +} 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..0c153d62aa79d --- /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..ddd8c70a97972 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php @@ -0,0 +1,534 @@ +install(); + $this->assertEmpty( $this->files_in_dir( static::$fonts_dir ), 'Font directory should be empty' ); + $this->assertInstanceOf( WP_Post::class, $font->get_font_post(), '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_font_post(), '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__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + } elseif ( 'font/woff' === $file['type'] ) { + copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + } elseif ( 'font/woff2' === $file['type'] ) { + copy( __DIR__ . '/../../../data/fonts/DMSans.woff2', $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' ), + ), + ); + } + + /** + * @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__ . '/../../../data/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__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + } elseif ( 'font/woff' === $file['type'] ) { + copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + } + } + foreach ( $files_data_overwrite as $file ) { + if ( 'font/ttf' === $file['type'] ) { + copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + } elseif ( 'font/woff' === $file['type'] ) { + copy( __DIR__ . '/../../../data/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..c878dd00fdb5c --- /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( __DIR__ . '/../../../data/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_font_post(), '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_font_post(), '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( __DIR__ . '/../../../data/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..0bd5b47ea2378 --- /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..e30c199612b8a --- /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..21517a0970f93 --- /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..bfdb7258fa11a --- /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..97e66e64e8716 --- /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..4bbafc55a2147 --- /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/registerFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php new file mode 100644 index 0000000000000..6bc5fbb8161ce --- /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..be15ecce89881 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php @@ -0,0 +1,30 @@ + '/abc', + 'basedir' => '/var/www/html/wp-content', + 'baseurl' => 'http://example.com/wp-content', + ); + $expected = array( + 'subdir' => '/fonts', + 'basedir' => '/var/www/html/wp-content', + 'baseurl' => content_url(), + 'path' => '/var/www/html/wp-content/fonts', + 'url' => content_url() . '/fonts', + ); + $this->assertSame( $expected, WP_Font_Library::set_upload_dir( $defaults ) ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php new file mode 100644 index 0000000000000..f1e712bab85b4 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php @@ -0,0 +1,34 @@ +factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $admin_id ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + parent::tear_down(); + + // Reset $collections static property of WP_Font_Library class. + $reflection = new ReflectionClass( 'WP_Font_Library' ); + $property = $reflection->getProperty( 'collections' ); + $property->setAccessible( true ); + $property->setValue( array() ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php new file mode 100644 index 0000000000000..dc8d640f86330 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/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://localhost/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://localhost/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/wpRestFontLibraryController/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php new file mode 100644 index 0000000000000..ad120ee36fce4 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php @@ -0,0 +1,45 @@ +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/wpRestFontLibraryController/installFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php new file mode 100644 index 0000000000000..e92776b70ed64 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php @@ -0,0 +1,420 @@ +set_param( 'fontFamilies', $font_families_json ); + $install_request->set_file_params( $files ); + $response = rest_get_server()->dispatch( $install_request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); + $this->assertCount( count( $expected_response ), $data, 'Not all the font families were installed correctly.' ); + + // Checks that the font families were installed correctly. + for ( $family_index = 0; $family_index < count( $data ); $family_index++ ) { + $installed_font = $data[ $family_index ]; + $expected_font = $expected_response[ $family_index ]; + + if ( isset( $installed_font['fontFace'] ) || isset( $expected_font['fontFace'] ) ) { + for ( $face_index = 0; $face_index < count( $installed_font['fontFace'] ); $face_index++ ) { + // Checks that the font asset were created correctly. + if ( isset( $installed_font['fontFace'][ $face_index ]['src'] ) ) { + $this->assertStringEndsWith( $expected_font['fontFace'][ $face_index ]['src'], $installed_font['fontFace'][ $face_index ]['src'], 'The src of the fonts were not updated as expected.' ); + } + // Removes the src from the response to compare the rest of the data. + unset( $installed_font['fontFace'][ $face_index ]['src'] ); + unset( $expected_font['fontFace'][ $face_index ]['src'] ); + unset( $installed_font['fontFace'][ $face_index ]['uploadedFile'] ); + } + } + + // Compares if the rest of the data is the same. + $this->assertEquals( $expected_font, $installed_font, 'The endpoint answer is not as expected.' ); + } + } + + /** + * Data provider for test_install_fonts + */ + public function data_install_fonts() { + + $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); + file_put_contents( $temp_file_path1, 'Mocking file content' ); + $temp_file_path2 = wp_tempnam( 'Monteserrat-' ); + file_put_contents( $temp_file_path2, 'Mocking file content' ); + + return array( + + 'google_fonts_to_download' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + ), + ), + ), + ), + 'files' => array(), + 'expected_response' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', + ), + ), + ), + ), + ), + + 'google_fonts_to_use_as_is' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + ), + ), + ), + ), + 'files' => array(), + 'expected_response' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + + ), + ), + ), + ), + ), + + 'fonts_without_font_faces' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', + ), + ), + 'files' => array(), + 'expected_response' => array( + array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', + ), + ), + ), + + 'fonts_with_local_fonts_assets' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'uploadedFile' => 'files1', + ), + ), + ), + ), + 'files' => array( + 'files0' => array( + 'name' => 'piazzola1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => $temp_file_path1, + 'error' => 0, + 'size' => 123, + ), + 'files1' => array( + 'name' => 'montserrat1.ttf', + 'type' => 'font/ttf', + 'tmp_name' => $temp_file_path2, + 'error' => 0, + 'size' => 123, + ), + ), + 'expected_response' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', + ), + ), + ), + ), + ), + ); + } + + /** + * Tests failure when fonfaces has improper inputs + * + * @dataProvider data_install_with_improper_inputs + * + * @param array $font_families Font families to install in theme.json format. + * @param array $files Font files to install. + */ + public function test_install_with_improper_inputs( $font_families, $files = array() ) { + $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); + $font_families_json = json_encode( $font_families ); + $install_request->set_param( 'fontFamilies', $font_families_json ); + $install_request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $install_request ); + $this->assertSame( 400, $response->get_status() ); + } + + /** + * Data provider for test_install_with_improper_inputs + */ + public function data_install_with_improper_inputs() { + $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); + file_put_contents( $temp_file_path1, 'Mocking file content' ); + + return array( + 'not a font families array' => array( + 'font_families' => 'This is not an array', + ), + + 'empty array' => array( + 'font_families' => array(), + ), + + 'without slug' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + ), + ), + ), + + 'with improper font face property' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => 'This is not an array', + ), + ), + ), + + 'with empty font face property' => array( + 'font_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array(), + ), + ), + ), + + 'fontface referencing uploaded file without uploaded files' => array( + 'font_families' => array( + 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_families' => array( + 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_families' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'uploadedFile' => 'files0', + ), + ), + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php new file mode 100644 index 0000000000000..2ac7b93c3a414 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php @@ -0,0 +1,28 @@ +get_routes(); + $this->assertArrayHasKey( '/wp/v2/fonts', $routes, 'Rest server has not the fonts path intialized.' ); + $this->assertCount( 2, $routes['/wp/v2/fonts'], 'Rest server has not the 2 fonts paths initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/fonts/collections'], 'Rest server has not the collections path initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/fonts/collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); + + $this->assertArrayHasKey( 'POST', $routes['/wp/v2/fonts'][0]['methods'], 'Rest server has not the POST method for fonts intialized.' ); + $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/fonts'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php new file mode 100644 index 0000000000000..3082bfc87f62e --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php @@ -0,0 +1,97 @@ + 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), + ), + ), + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + ), + ), + ), + ); + + $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); + $font_families_json = json_encode( $mock_families ); + $install_request->set_param( 'fontFamilies', $font_families_json ); + rest_get_server()->dispatch( $install_request ); + } + + public function test_uninstall() { + $font_families_to_uninstall = array( + array( + 'slug' => 'piazzolla', + ), + array( + 'slug' => 'montserrat', + ), + ); + + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); + $uninstall_request->set_param( 'fontFamilies', $font_families_to_uninstall ); + $response = rest_get_server()->dispatch( $uninstall_request ); + echo ( print_r( $response->get_data(), true ) ); + $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/fonts' ); + + $non_existing_font_data = array( + array( + 'slug' => 'non-existing-font', + 'name' => 'Non existing font', + ), + array( + 'slug' => 'another-not-installed-font', + 'name' => 'Another not installed font', + ), + ); + + $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data ); + $response = rest_get_server()->dispatch( $uninstall_request ); + $response->get_data(); + $this->assertSame( 500, $response->get_status(), 'The response status is not 500.' ); + } +} From 9e06a930f74dd1c0ba29bb5c4ef661e0d5227955 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 16:24:59 -0300 Subject: [PATCH 006/105] fix register post type for font family post type --- src/wp-includes/post.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 702136720aca9..7251d4eb2cfa5 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -561,10 +561,27 @@ 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. */ - 'label' => __( 'Font Library' ), - 'show_in_rest' => true, + '_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, + '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', + ), ) ); From b323b67e3376ca35101e35f2101c0501652ef4bf Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 16:34:29 -0300 Subject: [PATCH 007/105] move the font library functions --- src/wp-includes/font-library.php | 27 +++++++++++ .../fonts/font-library/font-library.php | 48 ------------------- 2 files changed, 27 insertions(+), 48 deletions(-) create mode 100644 src/wp-includes/font-library.php delete mode 100644 src/wp-includes/fonts/font-library/font-library.php diff --git a/src/wp-includes/font-library.php b/src/wp-includes/font-library.php new file mode 100644 index 0000000000000..e1347134e2f0e --- /dev/null +++ b/src/wp-includes/font-library.php @@ -0,0 +1,27 @@ + + 'default-font-collection', - 'name' => 'Google Fonts', - 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'default' ), - /* This will be changed to a wporg CDN URL, see: https://meta.trac.wordpress.org/ticket/7282#ticket */ - 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', // -); - -wp_register_font_collection( $default_font_collection ); From 715eea971d998e2b9282c9685617e23df04e68f5 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 16:35:08 -0300 Subject: [PATCH 008/105] avoid loading file deleted --- src/wp-settings.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-settings.php b/src/wp-settings.php index 77deabd71c424..257ba56fbfdf8 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -369,7 +369,6 @@ require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family-utils.php'; require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family.php'; require ABSPATH . WPINC . '/fonts/font-library/class-wp-rest-font-library-controller.php'; -require ABSPATH . WPINC . '/fonts/font-library/font-library.php'; $GLOBALS['wp_embed'] = new WP_Embed(); From e54426af9c6cfac667c253ef98c3d1f1144361a0 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 17:17:46 -0300 Subject: [PATCH 009/105] add wp_register_default_font_collection function --- src/wp-includes/font-library.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/font-library.php b/src/wp-includes/font-library.php index e1347134e2f0e..50892802a1480 100644 --- a/src/wp-includes/font-library.php +++ b/src/wp-includes/font-library.php @@ -1,4 +1,3 @@ - 'default-font-collection', + 'name' => 'Google Fonts', + 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), + /* TODO: This URL needs to change from the raw file to wporg CDN URL. */ + 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', + ) + ); +} From 10787679f0bccf646b8ce0259ba56538adeb5435 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 17:18:06 -0300 Subject: [PATCH 010/105] load font-library functions file --- src/wp-settings.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-settings.php b/src/wp-settings.php index 257ba56fbfdf8..18c150fe8e744 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -369,6 +369,7 @@ require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family-utils.php'; require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family.php'; require ABSPATH . WPINC . '/fonts/font-library/class-wp-rest-font-library-controller.php'; +require ABSPATH . WPINC . '/font-library.php'; $GLOBALS['wp_embed'] = new WP_Embed(); From 699f45bbe98ab89b2c1672dd85f6522168d2cf9a Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 17:19:17 -0300 Subject: [PATCH 011/105] register default font collection with init hook --- src/wp-includes/default-filters.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index a3ea1537cb1d0..22216feb1b9dc 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -724,4 +724,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 ); From a6dca3320e007b26b8504f18ac4976792c9cb41e Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 22 Sep 2023 17:44:31 -0300 Subject: [PATCH 012/105] fix rest-schema test --- tests/phpunit/tests/rest-api/rest-schema-setup.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index c33e9d5190c11..2c19eadd631f9 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -133,6 +133,9 @@ 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/fonts', + '/wp/v2/fonts/collections', + '/wp/v2/fonts/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', @@ -188,10 +191,6 @@ public function test_expected_routes_in_schema() { '/wp-site-health/v1/directory-sizes', '/wp/v2/wp_pattern_category', '/wp/v2/wp_pattern_category/(?P[\d]+)', - '/wp/v2/fonts', - '/wp/v2/fonts/collections', - '/wp/v2/fonts/collections/(?P[\/\w-]+)', - '' ); $this->assertSameSets( $expected_routes, $routes ); From 50841803f9e57036d801e27977f9302e8951f517 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 00:33:34 -0300 Subject: [PATCH 013/105] settings the right 'supports' values for wp_font_family post type --- src/wp-includes/post.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 7251d4eb2cfa5..688c681b2eebc 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -567,7 +567,7 @@ function create_initial_post_types() { '_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, + 'show_in_rest' => true, 'rewrite' => false, 'capabilities' => array( 'read' => 'edit_theme_options', @@ -581,6 +581,8 @@ function create_initial_post_types() { 'map_meta_cap' => true, 'supports' => array( 'title', + 'slug', + 'editor', ), ) ); From 46a1e9ff6586180cd39fda6e3750dd73db609d9f Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 01:02:48 -0300 Subject: [PATCH 014/105] set hierarchical as false for wp_font_family post type --- src/wp-includes/post.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 688c681b2eebc..32fe4fe553849 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -569,6 +569,7 @@ function create_initial_post_types() { 'show_ui' => false, 'show_in_rest' => true, 'rewrite' => false, + 'hierarchical' => false, 'capabilities' => array( 'read' => 'edit_theme_options', 'create_posts' => 'edit_theme_options', From 7e666e69364d2a4d3d843750e4a75acefa42bac1 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 01:55:46 -0300 Subject: [PATCH 015/105] fix url in test --- .../tests/fonts/font-library/wpFontCollection/getData.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php index 4d0b2eb92b595..4e3b1cd78b0a9 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php @@ -28,7 +28,7 @@ public function tear_down() { public function mock_request( $preempt, $args, $url ) { // if the URL is not the URL you want to mock, return false. - if ( 'https://localhost/fonts/mock-font-collection.json' !== $url ) { + if ( 'https://wordpress.org/fonts/mock-font-collection.json' !== $url ) { return false; } @@ -86,7 +86,7 @@ public function data_should_get_data() { 'id' => 'my-collection-with-url', 'name' => 'My Collection with URL', 'description' => 'My collection description', - 'src' => 'https://localhost/fonts/mock-font-collection.json', + 'src' => 'https://wordpress.org/fonts/mock-font-collection.json', ), 'expected_data' => array( 'id' => 'my-collection-with-url', From e6cce4387f42a32acf7892fd4b089983673dc754 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 15:32:56 -0300 Subject: [PATCH 016/105] fix set_upload_dir unit test --- .../fonts/font-library/wpFontLibrary/setUploadDir.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php index be15ecce89881..daa4c84aad900 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php @@ -15,14 +15,16 @@ class Tests_Fonts_WpFontLibrary_SetUploadDir extends WP_UnitTestCase { public function test_should_set_fonts_upload_dir() { $defaults = array( 'subdir' => '/abc', - 'basedir' => '/var/www/html/wp-content', - 'baseurl' => 'http://example.com/wp-content', + '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' => '/var/www/html/wp-content', + 'basedir' => WP_CONTENT_DIR, 'baseurl' => content_url(), - 'path' => '/var/www/html/wp-content/fonts', + 'path' => path_join( WP_CONTENT_DIR, 'fonts' ), 'url' => content_url() . '/fonts', ); $this->assertSame( $expected, WP_Font_Library::set_upload_dir( $defaults ) ); From 673c04201444105f324ba244e4ceb178a3acf1aa Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 17:07:50 -0300 Subject: [PATCH 017/105] fix unit test by using solvable host name --- .../wpRestFontLibraryController/getFontCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php index dc8d640f86330..f1a0a6a0cd510 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php @@ -36,7 +36,7 @@ public function set_up() { 'id' => 'collection-with-url', 'name' => 'Another Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => 'https://localhost/fonts/mock-font-collection.json', + 'src' => 'https://wordpress.org/fonts/mock-font-collection.json', ); wp_register_font_collection( $config_with_url ); @@ -62,7 +62,7 @@ public function set_up() { public function mock_request( $preempt, $args, $url ) { // Check if it's the URL you want to mock. - if ( 'https://localhost/fonts/mock-font-collection.json' === $url ) { + if ( 'https://wordpress.org/fonts/mock-font-collection.json' === $url ) { // Mock the response body. $mock_collection_data = array( From 72bbec56e5b1a9577fc1c00acffb7aa7190f3df9 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 17:17:36 -0300 Subject: [PATCH 018/105] avoid using gutenberg class, use core class instead --- src/wp-includes/fonts/font-library/class-wp-font-family.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family.php b/src/wp-includes/fonts/font-library/class-wp-font-family.php index ad7b0d207cd0d..366de407a240d 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-family.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-family.php @@ -306,7 +306,7 @@ private function sanitize() { ); // Creates a new WP_Theme_JSON object with the new fonts to // leverage sanitization and validation. - $theme_json = new WP_Theme_JSON_Gutenberg( $fonts_json ); + $theme_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] From 1d8de84bb99ce0dac5bfd09c76eb5e0109f18348 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Sat, 23 Sep 2023 17:50:10 -0300 Subject: [PATCH 019/105] add the font assets for the tests --- tests/phpunit/tests/data/fonts/DMSans.woff2 | Bin 0 -> 18212 bytes .../phpunit/tests/data/fonts/Merriweather.ttf | Bin 0 -> 149120 bytes .../tests/data/fonts/cooper-hewitt.woff | Bin 0 -> 54091 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/phpunit/tests/data/fonts/DMSans.woff2 create mode 100644 tests/phpunit/tests/data/fonts/Merriweather.ttf create mode 100644 tests/phpunit/tests/data/fonts/cooper-hewitt.woff diff --git a/tests/phpunit/tests/data/fonts/DMSans.woff2 b/tests/phpunit/tests/data/fonts/DMSans.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9a7696df2ade09b5a17828b68f3246cc6bb0b4ba GIT binary patch literal 18212 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 literal 0 HcmV?d00001 From d769cc5d86ec83c575bd512a3715f4c7342dfa7e Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 00:02:40 -0300 Subject: [PATCH 020/105] Remove font files created after tests run --- .../wpRestFontLibraryController/base.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php index f1e712bab85b4..8dcf05b612044 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php @@ -7,9 +7,19 @@ */ abstract class WP_REST_Font_Library_Controller_UnitTestCase extends WP_UnitTestCase { + /** + * Fonts directory (in uploads). + * + * @var string + */ + protected static $fonts_dir; + + public function set_up() { parent::set_up(); + static::$fonts_dir = WP_Font_Library::get_fonts_dir(); + // Create a user with administrator role. $admin_id = $this->factory->user->create( array( @@ -30,5 +40,10 @@ public function tear_down() { $property = $reflection->getProperty( 'collections' ); $property->setAccessible( true ); $property->setValue( array() ); + + // Clean up the /fonts directory. + foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { + @unlink( $file ); + } } } From c4a31d2e0aefbc1e72d14266ba52993a6685b4e8 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 00:05:49 -0300 Subject: [PATCH 021/105] removing unwanted echo line --- .../font-library/wpRestFontLibraryController/uninstallFonts.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php index 3082bfc87f62e..fcfdd502e633a 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php @@ -70,7 +70,6 @@ public function test_uninstall() { $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); $uninstall_request->set_param( 'fontFamilies', $font_families_to_uninstall ); $response = rest_get_server()->dispatch( $uninstall_request ); - echo ( print_r( $response->get_data(), true ) ); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); } From 8933cb8a68f158f2701ee0c0448f6774c3393340 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 00:42:17 -0300 Subject: [PATCH 022/105] add the custom post type routes to the routes test --- tests/phpunit/tests/rest-api/rest-schema-setup.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 2c19eadd631f9..2e7b24338cf14 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -189,6 +189,10 @@ public function test_expected_routes_in_schema() { '/wp-site-health/v1/tests/authorization-header', '/wp-site-health/v1/tests/page-cache', '/wp-site-health/v1/directory-sizes', + '/wp/v2/wp_font_family', + '/wp/v2/wp_font_family/(?P[\d]+)', + '/wp/v2/wp_font_family/(?P[\d]+)/autosaves', + '/wp/v2/wp_font_family/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/wp_pattern_category', '/wp/v2/wp_pattern_category/(?P[\d]+)', ); From 9aeb0ba3f8985f88caf56634b5f89ce74664b742 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 00:45:15 -0300 Subject: [PATCH 023/105] Removing class re-declaration check --- .../fonts/font-library/class-wp-font-collection.php | 4 ---- .../fonts/font-library/class-wp-font-family-utils.php | 4 ---- src/wp-includes/fonts/font-library/class-wp-font-family.php | 4 ---- src/wp-includes/fonts/font-library/class-wp-font-library.php | 4 ---- .../font-library/class-wp-rest-font-library-controller.php | 4 ---- 5 files changed, 20 deletions(-) diff --git a/src/wp-includes/fonts/font-library/class-wp-font-collection.php b/src/wp-includes/fonts/font-library/class-wp-font-collection.php index 5b3702ae865d1..a693f1e8fb049 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-collection.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-collection.php @@ -9,10 +9,6 @@ * @since 6.4.0 */ -if ( class_exists( 'WP_Font_Collection' ) ) { - return; -} - /** * Font Collection class. * diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php b/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php index 200cfef22289f..c8322d96bc0fd 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php @@ -9,10 +9,6 @@ * @since 6.4.0 */ -if ( class_exists( 'WP_Font_Family_Utils' ) ) { - return; -} - /** * A class of utilities for working with the Font Library. * diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family.php b/src/wp-includes/fonts/font-library/class-wp-font-family.php index 366de407a240d..293d048735a6d 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-family.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-family.php @@ -9,10 +9,6 @@ * @since 6.4.0 */ -if ( class_exists( 'WP_Font_Family' ) ) { - return; -} - /** * Font Library class. * diff --git a/src/wp-includes/fonts/font-library/class-wp-font-library.php b/src/wp-includes/fonts/font-library/class-wp-font-library.php index d488e330486a7..3524690d343e5 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-library.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-library.php @@ -9,10 +9,6 @@ * @since 6.4.0 */ -if ( class_exists( 'WP_Font_Library' ) ) { - return; -} - /** * Font Library class. * diff --git a/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php b/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php index dcf94d93012a2..936679b1c8acc 100644 --- a/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php @@ -9,10 +9,6 @@ * @since 6.4.0 */ -if ( class_exists( 'WP_REST_Font_Library_Controller' ) ) { - return; -} - /** * Font Library Controller class. * From 1725825570c821fe1b82caf66caf31f6ade24583 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 00:49:52 -0300 Subject: [PATCH 024/105] format php --- src/wp-includes/font-library.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/font-library.php b/src/wp-includes/font-library.php index 50892802a1480..c5bdaeb2e80d6 100644 --- a/src/wp-includes/font-library.php +++ b/src/wp-includes/font-library.php @@ -22,7 +22,7 @@ * successfully, else WP_Error. */ function wp_register_font_collection( $config ) { - return WP_Font_Library::register_font_collection( $config ); + return WP_Font_Library::register_font_collection( $config ); } /** @@ -30,14 +30,14 @@ function wp_register_font_collection( $config ) { * * @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.', 'gutenberg' ), - /* TODO: This URL needs to change from the raw file to wporg CDN URL. */ - 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', - ) - ); +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.', 'gutenberg' ), + /* TODO: This URL needs to change from the raw file to wporg CDN URL. */ + 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', + ) + ); } From d4bce592255e10e6a237770a95363a63559b5c25 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 10:20:48 -0300 Subject: [PATCH 025/105] updating wp-api-generated.js schema --- tests/qunit/fixtures/wp-api-generated.js | 804 ++++++++++++++++++++++- 1 file changed, 802 insertions(+), 2 deletions(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 36cab80cc18e0..8b7b4a1286193 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -8785,7 +8785,8 @@ mockedApiResponse.Schema = { "wp_block": "wp_block", "wp_template": "wp_template", "wp_template_part": "wp_template_part", - "wp_navigation": "wp_navigation" + "wp_navigation": "wp_navigation", + "wp_font_family": "wp_font_family" } }, "required": false @@ -9754,6 +9755,685 @@ mockedApiResponse.Schema = { } ] }, + "/wp/v2/wp_font_family": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "allow_batch": { + "v1": true + }, + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "page": { + "description": "Current page of the collection.", + "type": "integer", + "default": 1, + "minimum": 1, + "required": false + }, + "per_page": { + "description": "Maximum number of items to be returned in result set.", + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 100, + "required": false + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string", + "required": false + }, + "after": { + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "modified_after": { + "description": "Limit response to posts modified after a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "before": { + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "modified_before": { + "description": "Limit response to posts modified before a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "exclude": { + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "include": { + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer", + "required": false + }, + "order": { + "description": "Order sort attribute ascending or descending.", + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "required": false + }, + "orderby": { + "description": "Sort collection by post attribute.", + "type": "string", + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title" + ], + "required": false + }, + "search_columns": { + "default": [], + "description": "Array of column names to be searched.", + "type": "array", + "items": { + "enum": [ + "post_title", + "post_content", + "post_excerpt" + ], + "type": "string" + }, + "required": false + }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + }, + "required": false + }, + "status": { + "default": "publish", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "publish", + "future", + "draft", + "pending", + "private", + "trash", + "auto-draft", + "inherit", + "request-pending", + "request-confirmed", + "request-failed", + "request-completed", + "any" + ], + "type": "string" + }, + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "allow_batch": { + "v1": true + }, + "args": { + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" + } + ] + } + }, + "/wp/v2/wp_font_family/(?P[\\d])": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "password": { + "description": "The password for the post if it is password protected.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Whether to bypass Trash and force deletion.", + "required": false + } + } + } + ] + }, + "/wp/v2/wp_font_family/(?P[\\d])/autosaves": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + } + ] + }, + "/wp/v2/wp_font_family/(?P[\\d])/autosaves/(?P[\\d])": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "id": { + "description": "The ID for the autosave.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, "/wp/v2/search": { "namespace": "wp/v2", "methods": [ @@ -11471,6 +12151,96 @@ mockedApiResponse.Schema = { } } }, + "/wp/v2/fonts": { + "namespace": "wp/v2", + "methods": [ + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "args": { + "fontFamilies": { + "type": "string", + "required": true + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "fontFamilies": { + "type": "array", + "description": "The font families to install.", + "minItems": 1, + "items": { + "required": true, + "type": "object", + "properties": { + "slug": { + "type": "string", + "description": "The font family slug.", + "required": true + } + } + }, + "required": true + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/fonts" + } + ] + } + }, + "/wp/v2/fonts/collections": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/fonts/collections" + } + ] + } + }, + "/wp/v2/fonts/collections/(?P[\\/\\w-])": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + } + ] + }, "site_logo": 0, "site_icon": 0, "site_icon_url": "" @@ -12505,7 +13275,37 @@ mockedApiResponse.TypesCollection = { } ] } - } + }, + "wp_font_family": { + "description": "Font Family definition for installed fonts.", + "hierarchical": false, + "has_archive": false, + "name": "Font Family", + "slug": "wp_font_family", + "icon": null, + "taxonomies": [], + "rest_base": "wp_font_family", + "rest_namespace": "wp/v2", + "_links": { + "collection": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/types" + } + ], + "wp:items": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } + } }; mockedApiResponse.TypeModel = { From ab706085e5ba565918c07c4fbac8f6daf9627ab5 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 10:42:07 -0300 Subject: [PATCH 026/105] Revert "updating wp-api-generated.js schema" This reverts commit d4bce592255e10e6a237770a95363a63559b5c25. --- tests/qunit/fixtures/wp-api-generated.js | 804 +---------------------- 1 file changed, 2 insertions(+), 802 deletions(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 8b7b4a1286193..36cab80cc18e0 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -8785,8 +8785,7 @@ mockedApiResponse.Schema = { "wp_block": "wp_block", "wp_template": "wp_template", "wp_template_part": "wp_template_part", - "wp_navigation": "wp_navigation", - "wp_font_family": "wp_font_family" + "wp_navigation": "wp_navigation" } }, "required": false @@ -9755,685 +9754,6 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/wp_font_family": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "allow_batch": { - "v1": true - }, - "args": { - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "page": { - "description": "Current page of the collection.", - "type": "integer", - "default": 1, - "minimum": 1, - "required": false - }, - "per_page": { - "description": "Maximum number of items to be returned in result set.", - "type": "integer", - "default": 10, - "minimum": 1, - "maximum": 100, - "required": false - }, - "search": { - "description": "Limit results to those matching a string.", - "type": "string", - "required": false - }, - "after": { - "description": "Limit response to posts published after a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "modified_after": { - "description": "Limit response to posts modified after a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "before": { - "description": "Limit response to posts published before a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "modified_before": { - "description": "Limit response to posts modified before a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "exclude": { - "description": "Ensure result set excludes specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "include": { - "description": "Limit result set to specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "offset": { - "description": "Offset the result set by a specific number of items.", - "type": "integer", - "required": false - }, - "order": { - "description": "Order sort attribute ascending or descending.", - "type": "string", - "default": "desc", - "enum": [ - "asc", - "desc" - ], - "required": false - }, - "orderby": { - "description": "Sort collection by post attribute.", - "type": "string", - "default": "date", - "enum": [ - "author", - "date", - "id", - "include", - "modified", - "parent", - "relevance", - "slug", - "include_slugs", - "title" - ], - "required": false - }, - "search_columns": { - "default": [], - "description": "Array of column names to be searched.", - "type": "array", - "items": { - "enum": [ - "post_title", - "post_content", - "post_excerpt" - ], - "type": "string" - }, - "required": false - }, - "slug": { - "description": "Limit result set to posts with one or more specific slugs.", - "type": "array", - "items": { - "type": "string" - }, - "required": false - }, - "status": { - "default": "publish", - "description": "Limit result set to posts assigned one or more statuses.", - "type": "array", - "items": { - "enum": [ - "publish", - "future", - "draft", - "pending", - "private", - "trash", - "auto-draft", - "inherit", - "request-pending", - "request-confirmed", - "request-failed", - "request-completed", - "any" - ], - "type": "string" - }, - "required": false - } - } - }, - { - "methods": [ - "POST" - ], - "allow_batch": { - "v1": true - }, - "args": { - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "content": { - "description": "The content for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Content for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML content for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - } - } - } - ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" - } - ] - } - }, - "/wp/v2/wp_font_family/(?P[\\d])": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "allow_batch": { - "v1": true - }, - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "password": { - "description": "The password for the post if it is password protected.", - "type": "string", - "required": false - } - } - }, - { - "methods": [ - "POST", - "PUT", - "PATCH" - ], - "allow_batch": { - "v1": true - }, - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "content": { - "description": "The content for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Content for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML content for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - } - } - }, - { - "methods": [ - "DELETE" - ], - "allow_batch": { - "v1": true - }, - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "force": { - "type": "boolean", - "default": false, - "description": "Whether to bypass Trash and force deletion.", - "required": false - } - } - } - ] - }, - "/wp/v2/wp_font_family/(?P[\\d])/autosaves": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - }, - { - "methods": [ - "POST" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "content": { - "description": "The content for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Content for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML content for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - } - } - } - ] - }, - "/wp/v2/wp_font_family/(?P[\\d])/autosaves/(?P[\\d])": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "id": { - "description": "The ID for the autosave.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ] - }, "/wp/v2/search": { "namespace": "wp/v2", "methods": [ @@ -12151,96 +11471,6 @@ mockedApiResponse.Schema = { } } }, - "/wp/v2/fonts": { - "namespace": "wp/v2", - "methods": [ - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "endpoints": [ - { - "methods": [ - "POST", - "PUT", - "PATCH" - ], - "args": { - "fontFamilies": { - "type": "string", - "required": true - } - } - }, - { - "methods": [ - "DELETE" - ], - "args": { - "fontFamilies": { - "type": "array", - "description": "The font families to install.", - "minItems": 1, - "items": { - "required": true, - "type": "object", - "properties": { - "slug": { - "type": "string", - "description": "The font family slug.", - "required": true - } - } - }, - "required": true - } - } - } - ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/fonts" - } - ] - } - }, - "/wp/v2/fonts/collections": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": [] - } - ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/fonts/collections" - } - ] - } - }, - "/wp/v2/fonts/collections/(?P[\\/\\w-])": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": [] - } - ] - }, "site_logo": 0, "site_icon": 0, "site_icon_url": "" @@ -13275,37 +12505,7 @@ mockedApiResponse.TypesCollection = { } ] } - }, - "wp_font_family": { - "description": "Font Family definition for installed fonts.", - "hierarchical": false, - "has_archive": false, - "name": "Font Family", - "slug": "wp_font_family", - "icon": null, - "taxonomies": [], - "rest_base": "wp_font_family", - "rest_namespace": "wp/v2", - "_links": { - "collection": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/types" - } - ], - "wp:items": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - } + } }; mockedApiResponse.TypeModel = { From 739edeb931c5acf623a05a895eeb6ed0b9147d7c Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 10:52:29 -0300 Subject: [PATCH 027/105] moving font library rest controller to the endpoints folder --- .../endpoints}/class-wp-rest-font-library-controller.php | 0 src/wp-settings.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/wp-includes/{fonts/font-library => rest-api/endpoints}/class-wp-rest-font-library-controller.php (100%) diff --git a/src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php similarity index 100% rename from src/wp-includes/fonts/font-library/class-wp-rest-font-library-controller.php rename to src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php diff --git a/src/wp-settings.php b/src/wp-settings.php index 18c150fe8e744..7698b1dc4d21d 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -301,6 +301,7 @@ 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-library-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'; @@ -368,7 +369,6 @@ require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-library.php'; require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family-utils.php'; require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family.php'; -require ABSPATH . WPINC . '/fonts/font-library/class-wp-rest-font-library-controller.php'; require ABSPATH . WPINC . '/font-library.php'; $GLOBALS['wp_embed'] = new WP_Embed(); From 534264e4182246cb2054f292200ed228bc8e1900 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 10:59:37 -0300 Subject: [PATCH 028/105] Removing 'gutenberg' translation domain --- src/wp-includes/font-library.php | 2 +- .../font-library/class-wp-font-collection.php | 10 +++---- .../class-wp-font-family-utils.php | 2 +- .../font-library/class-wp-font-family.php | 10 +++---- .../class-wp-rest-font-library-controller.php | 30 +++++++++---------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/wp-includes/font-library.php b/src/wp-includes/font-library.php index c5bdaeb2e80d6..9965d26c9df6c 100644 --- a/src/wp-includes/font-library.php +++ b/src/wp-includes/font-library.php @@ -35,7 +35,7 @@ function wp_register_default_font_collection() { array( 'id' => 'default-font-collection', 'name' => 'Google Fonts', - 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), + 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.' ), /* TODO: This URL needs to change from the raw file to wporg CDN URL. */ 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', ) diff --git a/src/wp-includes/fonts/font-library/class-wp-font-collection.php b/src/wp-includes/fonts/font-library/class-wp-font-collection.php index a693f1e8fb049..99aed4d120f06 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-collection.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-collection.php @@ -77,26 +77,26 @@ public function get_data() { // If the src is a URL, fetch the data from the URL. if ( false !== strpos( $this->config['src'], 'http' ) && false !== strpos( $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.', 'gutenberg' ) ); + 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.', 'gutenberg' ) ); + 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.', 'gutenberg' ) ); + 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.', 'gutenberg' ) ); + 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.', 'gutenberg' ) ); + return new WP_Error( 'font_collection_read_error', __( 'Error reading the Font Collection data JSON file contents.' ) ); } } diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php b/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php index c8322d96bc0fd..0191290e394ae 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php @@ -54,7 +54,7 @@ public static function merge_fonts_data( $font1, $font2 ) { if ( $font1['slug'] !== $font2['slug'] ) { return new WP_Error( 'fonts_must_have_same_slug', - __( 'Fonts must have the same slug to be merged.', 'gutenberg' ) + __( 'Fonts must have the same slug to be merged.' ) ); } diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family.php b/src/wp-includes/fonts/font-library/class-wp-font-family.php index 293d048735a6d..f9b122b77e955 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-family.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-family.php @@ -104,7 +104,7 @@ public function uninstall() { if ( null === $post ) { return new WP_Error( 'font_family_not_found', - __( 'The font family could not be found.', 'gutenberg' ) + __( 'The font family could not be found.' ) ); } @@ -114,7 +114,7 @@ public function uninstall() { ) { return new WP_Error( 'font_family_not_deleted', - __( 'The font family could not be deleted.', 'gutenberg' ) + __( 'The font family could not be deleted.' ) ); } @@ -486,7 +486,7 @@ private function create_font_post() { if ( 0 === $post_id || is_wp_error( $post_id ) ) { return new WP_Error( 'font_post_creation_failed', - __( 'Font post creation failed', 'gutenberg' ) + __( 'Font post creation failed' ) ); } @@ -555,7 +555,7 @@ private function update_font_post( $post ) { if ( 0 === $post_id || is_wp_error( $post_id ) ) { return new WP_Error( 'font_post_update_failed', - __( 'Font post update failed', 'gutenberg' ) + __( 'Font post update failed' ) ); } @@ -600,7 +600,7 @@ public function install( $files = null ) { if ( ! $were_assets_written ) { return new WP_Error( 'font_face_download_failed', - __( 'The font face assets could not be written.', 'gutenberg' ) + __( 'The font face assets could not be written.' ) ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 936679b1c8acc..103db980cad24 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -143,13 +143,13 @@ private function get_validation_errors( $font_families, $files ) { $error_messages = array(); if ( ! is_array( $font_families ) ) { - $error_messages[] = __( 'fontFamilies should be an array of font families.', 'gutenberg' ); + $error_messages[] = __( 'fontFamilies should be an array of font families.' ); return $error_messages; } // Checks if there is at least one font family. if ( count( $font_families ) < 1 ) { - $error_messages[] = __( 'fontFamilies should have at least one font family definition.', 'gutenberg' ); + $error_messages[] = __( 'fontFamilies should have at least one font family definition.' ); return $error_messages; } @@ -163,7 +163,7 @@ private function get_validation_errors( $font_families, $files ) { ) { $error_messages[] = sprintf( // translators: 1: font family index. - __( 'Font family [%s] should have slug, name and fontFamily properties defined.', 'gutenberg' ), + __( 'Font family [%s] should have slug, name and fontFamily properties defined.' ), $family_index ); } @@ -172,7 +172,7 @@ private function get_validation_errors( $font_families, $files ) { if ( ! is_array( $font_family['fontFace'] ) ) { $error_messages[] = sprintf( // translators: 1: font family index. - __( 'Font family [%s] should have fontFace property defined as an array.', 'gutenberg' ), + __( 'Font family [%s] should have fontFace property defined as an array.' ), $family_index ); continue; @@ -181,7 +181,7 @@ private function get_validation_errors( $font_families, $files ) { if ( count( $font_family['fontFace'] ) < 1 ) { $error_messages[] = sprintf( // translators: 1: font family index. - __( 'Font family [%s] should have at least one font face definition.', 'gutenberg' ), + __( 'Font family [%s] should have at least one font face definition.' ), $family_index ); } @@ -193,7 +193,7 @@ private function get_validation_errors( $font_families, $files ) { if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { $error_messages[] = sprintf( // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), + __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.' ), $family_index, $face_index ); @@ -202,7 +202,7 @@ private function get_validation_errors( $font_families, $files ) { if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { $error_messages[] = sprintf( // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), + __( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.' ), $family_index, $face_index ); @@ -212,7 +212,7 @@ private function get_validation_errors( $font_families, $files ) { if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { $error_messages[] = sprintf( // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.', 'gutenberg' ), + __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.' ), $family_index, $face_index ); @@ -258,7 +258,7 @@ public function uninstall_schema() { return array( 'fontFamilies' => array( 'type' => 'array', - 'description' => __( 'The font families to install.', 'gutenberg' ), + 'description' => __( 'The font families to install.' ), 'required' => true, 'minItems' => 1, 'items' => array( @@ -267,7 +267,7 @@ public function uninstall_schema() { 'properties' => array( 'slug' => array( 'type' => 'string', - 'description' => __( 'The font family slug.', 'gutenberg' ), + 'description' => __( 'The font family slug.' ), 'required' => true, ), ), @@ -297,7 +297,7 @@ public function uninstall_fonts( $request ) { } } - return new WP_REST_Response( __( 'Font family uninstalled successfully.', 'gutenberg' ), 200 ); + return new WP_REST_Response( __( 'Font family uninstalled successfully.' ), 200 ); } /** @@ -311,7 +311,7 @@ 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.', 'gutenberg' ), + __( 'Sorry, you are not allowed to update the Font Library on this site.' ), array( 'status' => rest_authorization_required_code(), ) @@ -324,7 +324,7 @@ public function update_font_library_permissions_check() { if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { return new WP_Error( 'rest_cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ), + __( 'Error: WordPress does not have permission to write the fonts folder on your server.' ), array( 'status' => 500, ) @@ -360,7 +360,7 @@ public function install_fonts( $request ) { if ( empty( $fonts_to_install ) ) { return new WP_Error( 'no_fonts_to_install', - __( 'No fonts to install', 'gutenberg' ), + __( 'No fonts to install' ), array( 'status' => 400 ) ); } @@ -379,7 +379,7 @@ public function install_fonts( $request ) { if ( empty( $fonts_installed ) ) { return new WP_Error( 'error_installing_fonts', - __( 'Error installing fonts. No font was installed.', 'gutenberg' ), + __( 'Error installing fonts. No font was installed.' ), array( 'status' => 500 ) ); } From c2d38d685c9b1b0398cd4cb33daeeaaa8c116704 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 12:21:10 -0300 Subject: [PATCH 029/105] add comment about mime types logic --- src/wp-includes/fonts/font-library/class-wp-font-library.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wp-includes/fonts/font-library/class-wp-font-library.php b/src/wp-includes/fonts/font-library/class-wp-font-library.php index 3524690d343e5..97da963e5136a 100644 --- a/src/wp-includes/fonts/font-library/class-wp-font-library.php +++ b/src/wp-includes/fonts/font-library/class-wp-font-library.php @@ -16,6 +16,11 @@ */ class WP_Font_Library { + /** + * Fonts mime types allowed for each file type. + * Each file type can have multiple mime types depending on the PHP version. + * Currently wp_check_filetype_and_ext() only allows one mime type per file extension. + */ const PHP_7_TTF_MIME_TYPE = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; const ALLOWED_FONT_MIME_TYPES = array( From 2a715cd4e345ce30c64405eb0bc76ffd2634e9a8 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 12:44:11 -0300 Subject: [PATCH 030/105] using wordpress.org cdn to host google fonts collection data json file --- src/wp-includes/font-library.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-includes/font-library.php b/src/wp-includes/font-library.php index 9965d26c9df6c..d040ba73bf2c3 100644 --- a/src/wp-includes/font-library.php +++ b/src/wp-includes/font-library.php @@ -36,8 +36,7 @@ function wp_register_default_font_collection() { 'id' => 'default-font-collection', 'name' => 'Google Fonts', 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.' ), - /* TODO: This URL needs to change from the raw file to wporg CDN URL. */ - 'src' => 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/main/output/google-fonts-with-previews.json', + 'src' => 'https://s.w.org/images/fonts/16.7/collections/google-fonts-with-preview.json', ) ); } From 6bf7be307979a337a508a1265bbe6936ee650aa6 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 12:57:00 -0300 Subject: [PATCH 031/105] updating wp-api-generated.js fixtures --- tests/qunit/fixtures/wp-api-generated.js | 802 ++++++++++++++++++++++- 1 file changed, 801 insertions(+), 1 deletion(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 36cab80cc18e0..18c0956f97883 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -7416,6 +7416,685 @@ mockedApiResponse.Schema = { } ] }, + "/wp/v2/wp_font_family": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "allow_batch": { + "v1": true + }, + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "page": { + "description": "Current page of the collection.", + "type": "integer", + "default": 1, + "minimum": 1, + "required": false + }, + "per_page": { + "description": "Maximum number of items to be returned in result set.", + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 100, + "required": false + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string", + "required": false + }, + "after": { + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "modified_after": { + "description": "Limit response to posts modified after a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "before": { + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "modified_before": { + "description": "Limit response to posts modified before a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "exclude": { + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "include": { + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer", + "required": false + }, + "order": { + "description": "Order sort attribute ascending or descending.", + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "required": false + }, + "orderby": { + "description": "Sort collection by post attribute.", + "type": "string", + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title" + ], + "required": false + }, + "search_columns": { + "default": [], + "description": "Array of column names to be searched.", + "type": "array", + "items": { + "enum": [ + "post_title", + "post_content", + "post_excerpt" + ], + "type": "string" + }, + "required": false + }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + }, + "required": false + }, + "status": { + "default": "publish", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "publish", + "future", + "draft", + "pending", + "private", + "trash", + "auto-draft", + "inherit", + "request-pending", + "request-confirmed", + "request-failed", + "request-completed", + "any" + ], + "type": "string" + }, + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "allow_batch": { + "v1": true + }, + "args": { + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" + } + ] + } + }, + "/wp/v2/wp_font_family/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "password": { + "description": "The password for the post if it is password protected.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Whether to bypass Trash and force deletion.", + "required": false + } + } + } + ] + }, + "/wp/v2/wp_font_family/(?P[\\d]+)/autosaves": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + } + ] + }, + "/wp/v2/wp_font_family/(?P[\\d]+)/autosaves/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "id": { + "description": "The ID for the autosave.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, "/wp/v2/types": { "namespace": "wp/v2", "methods": [ @@ -8785,7 +9464,8 @@ mockedApiResponse.Schema = { "wp_block": "wp_block", "wp_template": "wp_template", "wp_template_part": "wp_template_part", - "wp_navigation": "wp_navigation" + "wp_navigation": "wp_navigation", + "wp_font_family": "wp_font_family" } }, "required": false @@ -11469,6 +12149,96 @@ mockedApiResponse.Schema = { } ] } + }, + "/wp/v2/fonts": { + "namespace": "wp/v2", + "methods": [ + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "args": { + "fontFamilies": { + "type": "string", + "required": true + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "fontFamilies": { + "type": "array", + "description": "The font families to install.", + "minItems": 1, + "items": { + "required": true, + "type": "object", + "properties": { + "slug": { + "type": "string", + "description": "The font family slug.", + "required": true + } + } + }, + "required": true + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/fonts" + } + ] + } + }, + "/wp/v2/fonts/collections": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/fonts/collections" + } + ] + } + }, + "/wp/v2/fonts/collections/(?P[\\/\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + } + ] } }, "site_logo": 0, @@ -12505,6 +13275,36 @@ mockedApiResponse.TypesCollection = { } ] } + }, + "wp_font_family": { + "description": "Font Family definition for installed fonts.", + "hierarchical": false, + "has_archive": false, + "name": "Font Family", + "slug": "wp_font_family", + "icon": null, + "taxonomies": [], + "rest_base": "wp_font_family", + "rest_namespace": "wp/v2", + "_links": { + "collection": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/types" + } + ], + "wp:items": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } } }; From bbb09e98645f9d97de48e74caff26b044977c853 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 13:58:46 -0300 Subject: [PATCH 032/105] avoid deprecation error in test --- .../fonts/font-library/wpRestFontLibraryController/base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php index 8dcf05b612044..99e1d01cad73b 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php @@ -39,7 +39,7 @@ public function tear_down() { $reflection = new ReflectionClass( 'WP_Font_Library' ); $property = $reflection->getProperty( 'collections' ); $property->setAccessible( true ); - $property->setValue( array() ); + $property->setValue( null, array() ); // Clean up the /fonts directory. foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { From 654dd1d12f325bfd6d8a6c794abe04fbdfd28ccc Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 15:53:40 -0300 Subject: [PATCH 033/105] Moving default font collection registration to font.php file Co-authored-by: andrewserong Co-authored-by: anton-vlasenko Co-authored-by: annezazu Co-authored-by: azaozz Co-authored-by: bph Co-authored-by: ciampo Co-authored-by: colorful-tones Co-authored-by: costdev Co-authored-by: DAreRodz Co-authored-by: dcalhoun Co-authored-by: derekblank Co-authored-by: felixarntz Co-authored-by: getdave Co-authored-by: github-actions[bot] Co-authored-by: glendaviesnz Co-authored-by: gutenbergplugin Co-authored-by: gziolo Co-authored-by: hellofromtonya Co-authored-by: iqbalpb Co-authored-by: ironprogrammer Co-authored-by: jameskoster Co-authored-by: jasmussen Co-authored-by: jeffikus Co-authored-by: jffng Co-authored-by: jordesign Co-authored-by: krokodok Co-authored-by: LittleBigThing Co-authored-by: luisherranz Co-authored-by: madhusudhand Co-authored-by: Mamaduka Co-authored-by: margolisj Co-authored-by: matiasbenedetto Co-authored-by: mikachan Co-authored-by: mtias Co-authored-by: ntsekouras Co-authored-by: oandregal Co-authored-by: ocean90 Co-authored-by: ockham Co-authored-by: paaljoachim Co-authored-by: pbking Co-authored-by: priethor Co-authored-by: properlypurple Co-authored-by: ramonjd Co-authored-by: Ren2049 Co-authored-by: richtabor Co-authored-by: spacedmonkey Co-authored-by: t-hamano Co-authored-by: TimothyBJacobs Co-authored-by: tomxygen Co-authored-by: vcanales Co-authored-by: walbo Co-authored-by: webd-uk Co-authored-by: youknowriad --- src/wp-includes/font-library.php | 42 -------------------------------- src/wp-includes/fonts.php | 35 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 42 deletions(-) delete mode 100644 src/wp-includes/font-library.php diff --git a/src/wp-includes/font-library.php b/src/wp-includes/font-library.php deleted file mode 100644 index d040ba73bf2c3..0000000000000 --- a/src/wp-includes/font-library.php +++ /dev/null @@ -1,42 +0,0 @@ - '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.php b/src/wp-includes/fonts.php index 306364bdc8099..4bd810a52ebb9 100644 --- a/src/wp-includes/fonts.php +++ b/src/wp-includes/fonts.php @@ -51,3 +51,38 @@ 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', + ) + ); +} + From 635d8703e9d570c92813a9e69e8b76915f7dc88a Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 16:02:04 -0300 Subject: [PATCH 034/105] moving font library files to the main fonts folder Co-authored-by: hellofromtonya --- .../fonts/{font-library => }/class-wp-font-collection.php | 0 .../{font-library => }/class-wp-font-family-utils.php | 0 .../fonts/{font-library => }/class-wp-font-family.php | 0 .../fonts/{font-library => }/class-wp-font-library.php | 0 src/wp-settings.php | 8 ++++---- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/wp-includes/fonts/{font-library => }/class-wp-font-collection.php (100%) rename src/wp-includes/fonts/{font-library => }/class-wp-font-family-utils.php (100%) rename src/wp-includes/fonts/{font-library => }/class-wp-font-family.php (100%) rename src/wp-includes/fonts/{font-library => }/class-wp-font-library.php (100%) diff --git a/src/wp-includes/fonts/font-library/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php similarity index 100% rename from src/wp-includes/fonts/font-library/class-wp-font-collection.php rename to src/wp-includes/fonts/class-wp-font-collection.php diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family-utils.php b/src/wp-includes/fonts/class-wp-font-family-utils.php similarity index 100% rename from src/wp-includes/fonts/font-library/class-wp-font-family-utils.php rename to src/wp-includes/fonts/class-wp-font-family-utils.php diff --git a/src/wp-includes/fonts/font-library/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php similarity index 100% rename from src/wp-includes/fonts/font-library/class-wp-font-family.php rename to src/wp-includes/fonts/class-wp-font-family.php diff --git a/src/wp-includes/fonts/font-library/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php similarity index 100% rename from src/wp-includes/fonts/font-library/class-wp-font-library.php rename to src/wp-includes/fonts/class-wp-font-library.php diff --git a/src/wp-settings.php b/src/wp-settings.php index 7698b1dc4d21d..0e80f50a317d0 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -365,10 +365,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/font-library/class-wp-font-collection.php'; -require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-library.php'; -require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family-utils.php'; -require ABSPATH . WPINC . '/fonts/font-library/class-wp-font-family.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'; require ABSPATH . WPINC . '/font-library.php'; $GLOBALS['wp_embed'] = new WP_Embed(); From 8714206ff657bbe975344a2d3e508f84a681be3d Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 25 Sep 2023 16:13:41 -0300 Subject: [PATCH 035/105] remove path from deleted file --- src/wp-settings.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-settings.php b/src/wp-settings.php index 0e80f50a317d0..86c3b525fd312 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -369,7 +369,6 @@ 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'; -require ABSPATH . WPINC . '/font-library.php'; $GLOBALS['wp_embed'] = new WP_Embed(); From 8c5ddec2ae5c9a632a5f3afbb8842eebd2953333 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Tue, 26 Sep 2023 12:05:52 -0300 Subject: [PATCH 036/105] replace strpos by str_contains --- src/wp-includes/fonts/class-wp-font-collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php index 99aed4d120f06..a0c0d7da9d22a 100644 --- a/src/wp-includes/fonts/class-wp-font-collection.php +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -75,7 +75,7 @@ public function get_config() { */ public function get_data() { // If the src is a URL, fetch the data from the URL. - if ( false !== strpos( $this->config['src'], 'http' ) && false !== strpos( $this->config['src'], '://' ) ) { + 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.' ) ); } From 04fe719b817b0c57e34442333a9c3744625cd2c4 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 27 Sep 2023 14:24:17 -0300 Subject: [PATCH 037/105] Testeable mime type conditionals porting https://github.com/WordPress/gutenberg/pull/54844 Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> --- .../fonts/class-wp-font-family-utils.php | 5 +- .../fonts/class-wp-font-family.php | 2 +- .../fonts/class-wp-font-library.php | 31 ++++--- .../wpFontLibrary/getMimeTypes.php | 90 +++++++++++++++++++ 4 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php diff --git a/src/wp-includes/fonts/class-wp-font-family-utils.php b/src/wp-includes/fonts/class-wp-font-family-utils.php index 0191290e394ae..e025e0d542c2f 100644 --- a/src/wp-includes/fonts/class-wp-font-family-utils.php +++ b/src/wp-includes/fonts/class-wp-font-family-utils.php @@ -81,8 +81,9 @@ public static function merge_fonts_data( $font1, $font2 ) { * @return bool True if the file has a font MIME type, false otherwise. */ public static function has_font_mime_type( $filepath ) { - $filetype = wp_check_filetype( $filepath, WP_Font_Library::ALLOWED_FONT_MIME_TYPES ); + $allowed_mime_types = WP_Font_Library::get_expected_font_mime_types_per_php_version(); + $filetype = wp_check_filetype( $filepath, $allowed_mime_types ); - return in_array( $filetype['type'], WP_Font_Library::ALLOWED_FONT_MIME_TYPES, true ); + return in_array( $filetype['type'], $allowed_mime_types, true ); } } diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index f9b122b77e955..247f5333056e6 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -178,7 +178,7 @@ private function get_upload_overrides( $filename ) { // 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::ALLOWED_FONT_MIME_TYPES, + 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), 'unique_filename_callback' => static function () use ( $filename ) { // Keep the original filename. return $filename; diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index 97da963e5136a..45ba253027937 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -17,18 +17,27 @@ class WP_Font_Library { /** - * Fonts mime types allowed for each file type. - * Each file type can have multiple mime types depending on the PHP version. - * Currently wp_check_filetype_and_ext() only allows one mime type per file extension. + * Provide the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. + * + * This is necessary until a collection of valid mime-types per-file extension can be provided to 'upload_mimes' filter. + * + * @since 6.4.0 + * + * @param array $php_version_id The version of PHP to provide mime types for. The default is the current PHP version. + * + * @return Array A collection of mime types keyed by file extension. */ - const PHP_7_TTF_MIME_TYPE = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; + public static function get_expected_font_mime_types_per_php_version( $php_version_id = PHP_VERSION_ID ) { + + $php_7_ttf_mime_type = $php_version_id >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; - const ALLOWED_FONT_MIME_TYPES = array( - 'otf' => 'font/otf', - 'ttf' => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : self::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', - ); + return array( + 'otf' => 'font/otf', + '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', + ); + } /** * Font collections. @@ -131,6 +140,6 @@ public static function set_upload_dir( $defaults ) { * @return array Modified upload directory. */ public static function set_allowed_mime_types( $mime_types ) { - return array_merge( $mime_types, self::ALLOWED_FONT_MIME_TYPES ); + return array_merge( $mime_types, self::get_expected_font_mime_types_per_php_version() ); } } 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..720695d8fe5ef --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -0,0 +1,90 @@ +assertEquals( $mimes, $expected ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_supply_correct_mime_type_for_php_version() { + return array( + 'version 7.2' => array( + 'php_version_id' => 70200, + 'expected' => array( + 'otf' => 'font/otf', + 'ttf' => 'application/x-font-ttf', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ), + ), + 'version 7.3' => array( + 'php_version_id' => 70300, + 'expected' => array( + 'otf' => 'font/otf', + 'ttf' => 'application/font-sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ), + ), + 'version 7.4' => array( + 'php_version_id' => 70400, + 'expected' => array( + 'otf' => 'font/otf', + 'ttf' => 'font/sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ), + ), + 'version 8.0' => array( + 'php_version_id' => 80000, + 'expected' => array( + 'otf' => 'font/otf', + 'ttf' => 'font/sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ), + ), + 'version 8.1' => array( + 'php_version_id' => 80100, + 'expected' => array( + 'otf' => 'font/otf', + 'ttf' => 'font/sfnt', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + ), + ), + 'version 8.2' => array( + 'php_version_id' => 80200, + 'expected' => array( + 'otf' => 'font/otf', + 'ttf' => 'font/sfnt', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + ), + ), + ); + } +} From 839ff7051b60eb6af45eea6f883100f2ac65923b Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 28 Sep 2023 13:12:53 -0300 Subject: [PATCH 038/105] Change expected OTF files mime type porting: https://github.com/WordPress/gutenberg/pull/54886 Co-authored-by: Madhu Dollu <1935113+madhusudhand@users.noreply.github.com> Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> --- .../fonts/class-wp-font-library.php | 2 +- .../tests/data/fonts/gilbert-color.otf | Bin 0 -> 626352 bytes .../font-library/wpFontFamily/install.php | 28 ++++++++++++++++++ .../wpFontLibrary/getMimeTypes.php | 12 ++++---- 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 tests/phpunit/tests/data/fonts/gilbert-color.otf diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index 45ba253027937..e7f0eaaefd543 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -32,7 +32,7 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio $php_7_ttf_mime_type = $php_version_id >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; return array( - 'otf' => 'font/otf', + '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', diff --git a/tests/phpunit/tests/data/fonts/gilbert-color.otf b/tests/phpunit/tests/data/fonts/gilbert-color.otf new file mode 100644 index 0000000000000000000000000000000000000000..f21f9a173f2fe0b53a02723ce7fae62fca7cb897 GIT binary patch literal 626352 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 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' ), + ), ); } diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 720695d8fe5ef..708134af69e92 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -34,7 +34,7 @@ public function data_should_supply_correct_mime_type_for_php_version() { 'version 7.2' => array( 'php_version_id' => 70200, 'expected' => array( - 'otf' => 'font/otf', + 'otf' => 'application/vnd.ms-opentype', 'ttf' => 'application/x-font-ttf', 'woff' => 'application/font-woff', 'woff2' => 'application/font-woff2', @@ -43,7 +43,7 @@ public function data_should_supply_correct_mime_type_for_php_version() { 'version 7.3' => array( 'php_version_id' => 70300, 'expected' => array( - 'otf' => 'font/otf', + 'otf' => 'application/vnd.ms-opentype', 'ttf' => 'application/font-sfnt', 'woff' => 'application/font-woff', 'woff2' => 'application/font-woff2', @@ -52,7 +52,7 @@ public function data_should_supply_correct_mime_type_for_php_version() { 'version 7.4' => array( 'php_version_id' => 70400, 'expected' => array( - 'otf' => 'font/otf', + 'otf' => 'application/vnd.ms-opentype', 'ttf' => 'font/sfnt', 'woff' => 'application/font-woff', 'woff2' => 'application/font-woff2', @@ -61,7 +61,7 @@ public function data_should_supply_correct_mime_type_for_php_version() { 'version 8.0' => array( 'php_version_id' => 80000, 'expected' => array( - 'otf' => 'font/otf', + 'otf' => 'application/vnd.ms-opentype', 'ttf' => 'font/sfnt', 'woff' => 'application/font-woff', 'woff2' => 'application/font-woff2', @@ -70,7 +70,7 @@ public function data_should_supply_correct_mime_type_for_php_version() { 'version 8.1' => array( 'php_version_id' => 80100, 'expected' => array( - 'otf' => 'font/otf', + 'otf' => 'application/vnd.ms-opentype', 'ttf' => 'font/sfnt', 'woff' => 'font/woff', 'woff2' => 'font/woff2', @@ -79,7 +79,7 @@ public function data_should_supply_correct_mime_type_for_php_version() { 'version 8.2' => array( 'php_version_id' => 80200, 'expected' => array( - 'otf' => 'font/otf', + 'otf' => 'application/vnd.ms-opentype', 'ttf' => 'font/sfnt', 'woff' => 'font/woff', 'woff2' => 'font/woff2', From 51164435b77deed771f8771c0e7bfb7535b91744 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 29 Sep 2023 18:43:25 -0300 Subject: [PATCH 039/105] standardize the output of install and uninstall endpoints Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> --- .../class-wp-rest-font-library-controller.php | 137 +++++++++++++----- 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 103db980cad24..cb22486c899df 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -9,6 +9,10 @@ * @since 6.4.0 */ +if ( class_exists( 'WP_REST_Font_Library_Controller' ) ) { + return; +} + /** * Font Library Controller class. * @@ -285,19 +289,39 @@ public function uninstall_schema() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function uninstall_fonts( $request ) { - $fonts_param = $request->get_param( 'fontFamilies' ); + $fonts_to_uninstall = $request->get_param( 'fontFamilies' ); + + $errors = array(); + $successes = array(); - foreach ( $fonts_param as $font_data ) { + if ( empty( $fonts_to_uninstall ) ) { + $errors[] = new WP_Error( + 'no_fonts_to_install', + __( 'No fonts to uninstall' ) + ); + $data = array( + 'successes' => $successes, + 'errors' => $errors, + ); + $response = rest_ensure_response( $data ); + $response->set_status( 400 ); + return $response; + } + + foreach ( $fonts_to_uninstall as $font_data ) { $font = new WP_Font_Family( $font_data ); $result = $font->uninstall(); - - // If there was an error uninstalling the font, return the error. if ( is_wp_error( $result ) ) { - return $result; + $errors[] = $result; + } else { + $successes[] = $result; } } - - return new WP_REST_Response( __( 'Font family uninstalled successfully.' ), 200 ); + $data = array( + 'successes' => $successes, + 'errors' => $errors, + ); + return rest_ensure_response( $data ); } /** @@ -317,23 +341,48 @@ public function update_font_library_permissions_check() { ) ); } + 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_upload_dir()['basedir']; + $upload_dir = WP_Font_Library::get_fonts_dir(); if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { - return new WP_Error( - 'rest_cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.' ), - array( - 'status' => 500, - ) - ); + return false; } - return true; } + /** + * Checks whether the request needs write permissions. + * + * @since 6.4.0 + * + * @param array[] $font_families Font families to install. + * @return bool Whether the request needs write permissions. + */ + private function needs_write_permission( $font_families ) { + foreach ( $font_families as $font ) { + if ( isset( $font['fontFace'] ) ) { + foreach ( $font['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. * @@ -357,38 +406,52 @@ public function install_fonts( $request ) { */ $fonts_to_install = json_decode( $fonts_param, true ); + $successes = array(); + $errors = array(); + $response_status = 200; + if ( empty( $fonts_to_install ) ) { - return new WP_Error( + $errors[] = new WP_Error( 'no_fonts_to_install', - __( 'No fonts to install' ), - array( 'status' => 400 ) + __( 'No fonts to install' ) ); + $response_status = 400; } - // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - - // Iterates the fonts data received and creates a new WP_Font_Family object for each one. - $fonts_installed = array(); - foreach ( $fonts_to_install as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $font->install( $files ); - $fonts_installed[] = $font; + if ( $this->needs_write_permission( $fonts_to_install ) && ! $this->has_write_permission() ) { + $errors[] = new WP_Error( + 'cannot_write_fonts_folder', + __( 'Error: WordPress does not have permission to write the fonts folder on your server.' ) + ); + $response_status = 500; } - if ( empty( $fonts_installed ) ) { - return new WP_Error( - 'error_installing_fonts', - __( 'Error installing fonts. No font was installed.' ), - array( 'status' => 500 ) + if ( ! empty( $errors ) ) { + $data = array( + 'successes' => $successes, + 'errors' => $errors, ); + $response = rest_ensure_response( $data ); + $response->set_status( $response_status ); + return $response; } - $response = array(); - foreach ( $fonts_installed as $font ) { - $response[] = $font->get_data(); + // Get uploaded files (used when installing local fonts). + $files = $request->get_file_params(); + foreach ( $fonts_to_install as $font_data ) { + $font = new WP_Font_Family( $font_data ); + $result = $font->install( $files ); + if ( is_wp_error( $result ) ) { + $errors[] = $result; + } else { + $successes[] = $result; + } } - return new WP_REST_Response( $response ); + $data = array( + 'successes' => $successes, + 'errors' => $errors, + ); + return rest_ensure_response( $data ); } } From d89e201455be54edee7a40c7b71fc8e3ddb8939d Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 29 Sep 2023 18:45:14 -0300 Subject: [PATCH 040/105] update install and uninstall enpoints tests for the new standard output format Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> --- .../installFonts.php | 161 ++++++++++-------- .../uninstallFonts.php | 4 +- 2 files changed, 89 insertions(+), 76 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php index e92776b70ed64..6ec15c71ade59 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php @@ -28,14 +28,13 @@ public function test_install_fonts( $font_families, $files, $expected_response ) $install_request->set_file_params( $files ); $response = rest_get_server()->dispatch( $install_request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertCount( count( $expected_response ), $data, 'Not all the font families were installed correctly.' ); + $this->assertCount( count( $expected_response['successes'] ), $data['successes'], 'Not all the font families were installed correctly.' ); // Checks that the font families were installed correctly. - for ( $family_index = 0; $family_index < count( $data ); $family_index++ ) { - $installed_font = $data[ $family_index ]; - $expected_font = $expected_response[ $family_index ]; + for ( $family_index = 0; $family_index < count( $data['successes'] ); $family_index++ ) { + $installed_font = $data['successes'][ $family_index ]; + $expected_font = $expected_response['successes'][ $family_index ]; if ( isset( $installed_font['fontFace'] ) || isset( $expected_font['fontFace'] ) ) { for ( $face_index = 0; $face_index < count( $installed_font['fontFace'] ); $face_index++ ) { @@ -61,9 +60,10 @@ public function test_install_fonts( $font_families, $files, $expected_response ) public function data_install_fonts() { $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); - file_put_contents( $temp_file_path1, 'Mocking file content' ); + copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path1 ); + $temp_file_path2 = wp_tempnam( 'Monteserrat-' ); - file_put_contents( $temp_file_path2, 'Mocking file content' ); + copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path2 ); return array( @@ -100,32 +100,35 @@ public function data_install_fonts() { ), 'files' => array(), 'expected_response' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + 'successes' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + ), ), ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', + ), ), ), ), + 'errors' => array(), ), ), @@ -160,33 +163,36 @@ public function data_install_fonts() { ), 'files' => array(), 'expected_response' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'successes' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + ), ), ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', + ), ), ), ), + 'errors' => array(), ), ), @@ -200,11 +206,14 @@ public function data_install_fonts() { ), 'files' => array(), 'expected_response' => array( - array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', + 'successes' => array( + array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', + ), ), + 'errors' => array(), ), ), @@ -254,32 +263,36 @@ public function data_install_fonts() { ), ), 'expected_response' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + 'successes' => array( + array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', + ), ), ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', + array( + 'fontFamily' => 'Montserrat', + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFace' => array( + array( + 'fontFamily' => 'Montserrat', + 'fontStyle' => 'normal', + 'fontWeight' => '100', + 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', + ), ), ), + ), + 'errors' => array(), ), ), ); diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php index fcfdd502e633a..860f47d876481 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php @@ -90,7 +90,7 @@ public function test_uninstall_non_existing_fonts() { $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data ); $response = rest_get_server()->dispatch( $uninstall_request ); - $response->get_data(); - $this->assertSame( 500, $response->get_status(), 'The response status is not 500.' ); + $data = $response->get_data(); + $this->assertCount( 2, $data['errors'], 'The response should have 2 errors, one for each font family uninstall failure.' ); } } From e4b2a852d6faf4befc4c91f390e04075776bc311 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 10:32:25 -0300 Subject: [PATCH 041/105] remove class_exist check coming from gutenberg --- .../endpoints/class-wp-rest-font-library-controller.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index cb22486c899df..365e0227694b5 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -9,10 +9,6 @@ * @since 6.4.0 */ -if ( class_exists( 'WP_REST_Font_Library_Controller' ) ) { - return; -} - /** * Font Library Controller class. * From 8e3e711d43fb9f17526af91d0b636c433f22097c Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 12:15:21 -0300 Subject: [PATCH 042/105] remove unused line Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --- src/wp-includes/fonts/class-wp-font-family.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index 247f5333056e6..1bb6587b46ed9 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -159,7 +159,6 @@ private static function delete_font_face_assets( $font_face ) { return true; } - /** * Gets the overrides for the 'wp_handle_upload' function. * From 947e2cb73b8b9d7ad6d7dc085cc16a58be6ac663 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 12:49:20 -0300 Subject: [PATCH 043/105] add trailing dot Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --- src/wp-includes/fonts/class-wp-font-family.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index 1bb6587b46ed9..5bfbbb3f3ce6b 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -485,7 +485,7 @@ private function create_font_post() { if ( 0 === $post_id || is_wp_error( $post_id ) ) { return new WP_Error( 'font_post_creation_failed', - __( 'Font post creation failed' ) + __( 'Font post creation failed.' ) ); } From 88ec9b7b841f547ba8d8b44744cb0ad6c90fc7f5 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 13:04:51 -0300 Subject: [PATCH 044/105] se snake_case instead of camelCase on fontFamilies endpoint param --- .../class-wp-rest-font-library-controller.php | 12 ++++++------ .../wpRestFontLibraryController/installFonts.php | 4 ++-- .../wpRestFontLibraryController/uninstallFonts.php | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 365e0227694b5..638dc4bbf5d3f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -41,7 +41,7 @@ public function register_routes() { 'callback' => array( $this, 'install_fonts' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), 'args' => array( - 'fontFamilies' => array( + 'font_families' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( $this, 'validate_install_font_families' ), @@ -143,13 +143,13 @@ private function get_validation_errors( $font_families, $files ) { $error_messages = array(); if ( ! is_array( $font_families ) ) { - $error_messages[] = __( 'fontFamilies should be an array of font families.' ); + $error_messages[] = __( 'font_families should be an array of font families.' ); return $error_messages; } // Checks if there is at least one font family. if ( count( $font_families ) < 1 ) { - $error_messages[] = __( 'fontFamilies should have at least one font family definition.' ); + $error_messages[] = __( 'font_families should have at least one font family definition.' ); return $error_messages; } @@ -256,7 +256,7 @@ public function validate_install_font_families( $param, $request ) { */ public function uninstall_schema() { return array( - 'fontFamilies' => array( + 'font_families' => array( 'type' => 'array', 'description' => __( 'The font families to install.' ), 'required' => true, @@ -285,7 +285,7 @@ public function uninstall_schema() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function uninstall_fonts( $request ) { - $fonts_to_uninstall = $request->get_param( 'fontFamilies' ); + $fonts_to_uninstall = $request->get_param( 'font_families' ); $errors = array(); $successes = array(); @@ -393,7 +393,7 @@ private function needs_write_permission( $font_families ) { */ public function install_fonts( $request ) { // Get new fonts to install. - $fonts_param = $request->get_param( 'fontFamilies' ); + $fonts_param = $request->get_param( 'font_families' ); /* * As this is receiving form data, the font families are encoded as a string. diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php index 6ec15c71ade59..01ac1ff8436ed 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php @@ -24,7 +24,7 @@ class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_ public function test_install_fonts( $font_families, $files, $expected_response ) { $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); $font_families_json = json_encode( $font_families ); - $install_request->set_param( 'fontFamilies', $font_families_json ); + $install_request->set_param( 'font_families', $font_families_json ); $install_request->set_file_params( $files ); $response = rest_get_server()->dispatch( $install_request ); $data = $response->get_data(); @@ -309,7 +309,7 @@ public function data_install_fonts() { public function test_install_with_improper_inputs( $font_families, $files = array() ) { $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); $font_families_json = json_encode( $font_families ); - $install_request->set_param( 'fontFamilies', $font_families_json ); + $install_request->set_param( 'font_families', $font_families_json ); $install_request->set_file_params( $files ); $response = rest_get_server()->dispatch( $install_request ); diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php index 860f47d876481..a3b613e6f983e 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php @@ -53,7 +53,7 @@ public function set_up() { $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); $font_families_json = json_encode( $mock_families ); - $install_request->set_param( 'fontFamilies', $font_families_json ); + $install_request->set_param( 'font_families', $font_families_json ); rest_get_server()->dispatch( $install_request ); } @@ -68,7 +68,7 @@ public function test_uninstall() { ); $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); - $uninstall_request->set_param( 'fontFamilies', $font_families_to_uninstall ); + $uninstall_request->set_param( 'font_families', $font_families_to_uninstall ); $response = rest_get_server()->dispatch( $uninstall_request ); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); } @@ -88,7 +88,7 @@ public function test_uninstall_non_existing_fonts() { ), ); - $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data ); + $uninstall_request->set_param( 'font_families', $non_existing_font_data ); $response = rest_get_server()->dispatch( $uninstall_request ); $data = $response->get_data(); $this->assertCount( 2, $data['errors'], 'The response should have 2 errors, one for each font family uninstall failure.' ); From e218ec8b94e679da12c013bf7ce617f169ee0738 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:00:52 -0300 Subject: [PATCH 045/105] using DIR_TESTDATA for file assets paths --- .../{tests => }/data/fonts/DMSans.woff2 | Bin .../{tests => }/data/fonts/Merriweather.ttf | Bin .../{tests => }/data/fonts/cooper-hewitt.woff | Bin .../{tests => }/data/fonts/gilbert-color.otf | Bin .../fonts/font-library/wpFontFamily/base.php | 2 +- .../font-library/wpFontFamily/install.php | 18 +++++++++--------- .../font-library/wpFontFamily/uninstall.php | 4 ++-- .../installFonts.php | 4 ++-- tests/qunit/fixtures/wp-api-generated.js | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) rename tests/phpunit/{tests => }/data/fonts/DMSans.woff2 (100%) rename tests/phpunit/{tests => }/data/fonts/Merriweather.ttf (100%) rename tests/phpunit/{tests => }/data/fonts/cooper-hewitt.woff (100%) rename tests/phpunit/{tests => }/data/fonts/gilbert-color.otf (100%) diff --git a/tests/phpunit/tests/data/fonts/DMSans.woff2 b/tests/phpunit/data/fonts/DMSans.woff2 similarity index 100% rename from tests/phpunit/tests/data/fonts/DMSans.woff2 rename to tests/phpunit/data/fonts/DMSans.woff2 diff --git a/tests/phpunit/tests/data/fonts/Merriweather.ttf b/tests/phpunit/data/fonts/Merriweather.ttf similarity index 100% rename from tests/phpunit/tests/data/fonts/Merriweather.ttf rename to tests/phpunit/data/fonts/Merriweather.ttf diff --git a/tests/phpunit/tests/data/fonts/cooper-hewitt.woff b/tests/phpunit/data/fonts/cooper-hewitt.woff similarity index 100% rename from tests/phpunit/tests/data/fonts/cooper-hewitt.woff rename to tests/phpunit/data/fonts/cooper-hewitt.woff diff --git a/tests/phpunit/tests/data/fonts/gilbert-color.otf b/tests/phpunit/data/fonts/gilbert-color.otf similarity index 100% rename from tests/phpunit/tests/data/fonts/gilbert-color.otf rename to tests/phpunit/data/fonts/gilbert-color.otf diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php index 3650ac7dab997..2906686c4543a 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php @@ -36,7 +36,7 @@ public function set_up() { parent::set_up(); $merriweather_tmp_name = wp_tempnam( 'Merriweather-' ); - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $merriweather_tmp_name ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $merriweather_tmp_name ); $this->merriweather = array( 'font_data' => array( 'name' => 'Merriweather', diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php index 79cb6ac3ba2ba..1562dd27cf7bf 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php @@ -162,13 +162,13 @@ public function test_should_move_local_fontfaces( $font_data, array $files_data, // Set up the temporary files. foreach ( $files_data as $file ) { if ( 'font/ttf' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); } elseif ( 'font/woff' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/cooper-hewitt.woff' ), $file['tmp_name'] ); } elseif ( 'font/woff2' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/DMSans.woff2', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/DMSans.woff2' ), $file['tmp_name'] ); } elseif ( 'application/vnd.ms-opentype' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/gilbert-color.otf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/gilbert-color.otf' ), $file['tmp_name'] ); } } @@ -343,7 +343,7 @@ public function data_should_move_local_fontfaces() { 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__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); } $font = new WP_Font_Family( $font_data ); @@ -533,16 +533,16 @@ public function test_should_overwrite_fontface_with_different_extension() { // Set up the temporary files. foreach ( $files_data_initial as $file ) { if ( 'font/ttf' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); } elseif ( 'font/woff' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + copy( path_join ( DIR_TESTDATA, 'fonts/cooper-hewitt.woff' ), $file['tmp_name'] ); } } foreach ( $files_data_overwrite as $file ) { if ( 'font/ttf' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); } elseif ( 'font/woff' === $file['type'] ) { - copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] ); + copy( path_join ( DIR_TESTDATA, 'fonts/cooper-hewitt.woff' ), $file['tmp_name'] ); } } diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php index c878dd00fdb5c..ded822346f970 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php @@ -72,7 +72,7 @@ public function data_should_return_error_when_not_able_to_uninstall() { public function test_should_uninstall( $font_data, array $files_data ) { // Set up. foreach ( $files_data as $file ) { - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); } $font = new WP_Font_Family( $font_data ); $font->install( $files_data ); @@ -103,7 +103,7 @@ public function test_should_uninstall_only_its_font_family( $font_data, array $f // Set up the font family to be uninstalled. foreach ( $files_data as $file ) { - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); } $font = new WP_Font_Family( $font_data ); $font->install( $files_data ); diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php index 01ac1ff8436ed..9dabd3491a27e 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php @@ -60,10 +60,10 @@ public function test_install_fonts( $font_families, $files, $expected_response ) public function data_install_fonts() { $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path1 ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $temp_file_path1 ); $temp_file_path2 = wp_tempnam( 'Monteserrat-' ); - copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path2 ); + copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $temp_file_path2 ); return array( diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 03bd2d108e9eb..1ae4f51c1221a 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12166,7 +12166,7 @@ mockedApiResponse.Schema = { "PATCH" ], "args": { - "fontFamilies": { + "font_families": { "type": "string", "required": true } @@ -12177,7 +12177,7 @@ mockedApiResponse.Schema = { "DELETE" ], "args": { - "fontFamilies": { + "font_families": { "type": "array", "description": "The font families to install.", "minItems": 1, From 02d6767deaa87f6b096c6a66d24f0a3c927cf2be Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:05:53 -0300 Subject: [PATCH 046/105] Remove extra new line Co-authored-by: Mukesh Panchal --- .../tests/fonts/font-library/wpFontLibrary/getMimeTypes.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 708134af69e92..3b75cde941cc9 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -13,7 +13,6 @@ class Tests_Fonts_WpFontsFamilyUtils_GetMimeTypes extends WP_UnitTestCase { /** - * * @dataProvider data_should_supply_correct_mime_type_for_php_version * * @param array $php_version_id PHP_VERSION_ID value. From 641e9f39c559589b6683d294ac3dcd8ea2b02076 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:10:03 -0300 Subject: [PATCH 047/105] emoving an unnecessary line break Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --- src/wp-includes/fonts/class-wp-font-library.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index e7f0eaaefd543..dfc954dd28d8f 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -28,7 +28,6 @@ class WP_Font_Library { * @return Array A collection of mime types keyed by file extension. */ public static function get_expected_font_mime_types_per_php_version( $php_version_id = PHP_VERSION_ID ) { - $php_7_ttf_mime_type = $php_version_id >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; return array( From 5178b1e5a9c3039edc32dfdc8a3917db5343c4e9 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:15:26 -0300 Subject: [PATCH 048/105] use self::factory() to insert post in test Co-authored-by: Jonny Harris --- .../tests/fonts/font-library/wpFontFamily/getFontPost.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php index eb42cc3ee0898..0d424e0c153d6 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php @@ -25,7 +25,7 @@ public function test_should_return_post() { 'post_content' => '', 'post_status' => 'publish', ); - $post_id = wp_insert_post( $post ); + $post_id = self::factory()->post->create( $post ); $font = new WP_Font_Family( $this->merriweather['font_data'] ); // Test. From 4612703bfcfc15783b1a88afd3ea4c6f30efbde8 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:17:18 -0300 Subject: [PATCH 049/105] avoid adding variable name in return comment Co-authored-by: Mukesh Panchal --- .../endpoints/class-wp-rest-font-library-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 638dc4bbf5d3f..fdd8b3bcf917b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -137,7 +137,7 @@ public function get_font_collections() { * * @param array[] $font_families Font families to install. * @param array $files Files to install. - * @return array $error_messages Array of error messages. + * @return array Array of error messages. */ private function get_validation_errors( $font_families, $files ) { $error_messages = array(); From f69e6e5ce28ecf2cbf205ccfe4713465e1d80e2e Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:20:53 -0300 Subject: [PATCH 050/105] update return type of function --- .../endpoints/class-wp-rest-font-library-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index fdd8b3bcf917b..1f3201ae98365 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -233,7 +233,7 @@ private function get_validation_errors( $font_families, $files ) { * * @param string $param The font families to install. * @param WP_REST_Request $request The request object. - * @return true|WP_Error True if the parameter is valid, WP_Error otherwise. + * @return bool|WP_Error True if the parameter is valid, WP_Error otherwise. */ public function validate_install_font_families( $param, $request ) { $font_families = json_decode( $param, true ); From 17c9fd335574f3e85df86e607d1d878531bb1c33 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:25:17 -0300 Subject: [PATCH 051/105] update comments --- src/wp-includes/fonts/class-wp-font-library.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index dfc954dd28d8f..ad46d27d58606 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -17,7 +17,7 @@ class WP_Font_Library { /** - * Provide the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. + * Provides the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. * * This is necessary until a collection of valid mime-types per-file extension can be provided to 'upload_mimes' filter. * @@ -48,7 +48,7 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio private static $collections = array(); /** - * Register a new font collection. + * Registers a new font collection. * * @since 6.4.0 * From 3021090f0c11781598d780e6997cb9bfb5ab12fb Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 14:26:29 -0300 Subject: [PATCH 052/105] move property definition on top of the class --- .../fonts/class-wp-font-library.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index ad46d27d58606..c35db26bbe00d 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -16,6 +16,15 @@ */ class WP_Font_Library { + /** + * Font collections. + * + * @since 6.4.0 + * + * @var array + */ + private static $collections = array(); + /** * Provides the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. * @@ -38,15 +47,6 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio ); } - /** - * Font collections. - * - * @since 6.4.0 - * - * @var array - */ - private static $collections = array(); - /** * Registers a new font collection. * From db11c1f23c523405f47821dad572d5dd9cbdcff6 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 15:06:33 -0300 Subject: [PATCH 053/105] replace EDITABLE by CREATABLE Co-authored-by: Timothy Jacobs --- .../endpoints/class-wp-rest-font-library-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 1f3201ae98365..cc04726ba0e44 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -37,7 +37,7 @@ public function register_routes() { '/' . $this->rest_base, array( array( - 'methods' => WP_REST_Server::EDITABLE, + 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'install_fonts' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), 'args' => array( From 31af89418561040781493b2504dd9b34c3abc805 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 15:20:08 -0300 Subject: [PATCH 054/105] move uninstall endpoint schema --- .../class-wp-rest-font-library-controller.php | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index cc04726ba0e44..99adc65802a22 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -59,9 +59,27 @@ public function register_routes() { 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'uninstall_fonts' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => $this->uninstall_schema(), + 'args' => array( + 'font_families' => array( + 'type' => 'array', + 'description' => __( 'The font families to uninstall.' ), + 'required' => true, + 'minItems' => 1, + 'items' => array( + 'required' => true, + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The font family slug.' ), + 'required' => true, + ), + ), + ), + ), + ), ), - ) + ), ); register_rest_route( @@ -247,35 +265,6 @@ public function validate_install_font_families( $param, $request ) { return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) ); } - /** - * Gets the schema for the uninstall endpoint. - * - * @since 6.4.0 - * - * @return array Schema array. - */ - public function uninstall_schema() { - return array( - 'font_families' => array( - 'type' => 'array', - 'description' => __( 'The font families to install.' ), - 'required' => true, - 'minItems' => 1, - 'items' => array( - 'required' => true, - 'type' => 'object', - 'properties' => array( - 'slug' => array( - 'type' => 'string', - 'description' => __( 'The font family slug.' ), - 'required' => true, - ), - ), - ), - ), - ); - } - /** * Removes font families from the Font Library and all their assets. * From 21e3d3398d26cd7ebff288b41d96cfe5cc6627db Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 15:31:31 -0300 Subject: [PATCH 055/105] updating wp-api-generated.js file --- tests/qunit/fixtures/wp-api-generated.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 1ae4f51c1221a..4af88d2ff930a 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12154,16 +12154,12 @@ mockedApiResponse.Schema = { "namespace": "wp/v2", "methods": [ "POST", - "PUT", - "PATCH", "DELETE" ], "endpoints": [ { "methods": [ - "POST", - "PUT", - "PATCH" + "POST" ], "args": { "font_families": { @@ -12179,7 +12175,7 @@ mockedApiResponse.Schema = { "args": { "font_families": { "type": "array", - "description": "The font families to install.", + "description": "The font families to uninstall.", "minItems": 1, "items": { "required": true, From 9aa43bf5ff8ef4de26d118f51407c81e94abbe81 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 15:33:25 -0300 Subject: [PATCH 056/105] Rename function Co-authored-by: Pascal Birchler <841956+swissspidy@users.noreply.github.com> --- src/wp-includes/fonts/class-wp-font-family-utils.php | 2 +- src/wp-includes/fonts/class-wp-font-family.php | 2 +- src/wp-includes/fonts/class-wp-font-library.php | 4 ++-- .../tests/fonts/font-library/wpFontLibrary/getMimeTypes.php | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family-utils.php b/src/wp-includes/fonts/class-wp-font-family-utils.php index e025e0d542c2f..e8811769783b9 100644 --- a/src/wp-includes/fonts/class-wp-font-family-utils.php +++ b/src/wp-includes/fonts/class-wp-font-family-utils.php @@ -81,7 +81,7 @@ public static function merge_fonts_data( $font1, $font2 ) { * @return bool True if the file has a font MIME type, false otherwise. */ public static function has_font_mime_type( $filepath ) { - $allowed_mime_types = WP_Font_Library::get_expected_font_mime_types_per_php_version(); + $allowed_mime_types = WP_Font_Library::get_font_mime_types(); $filetype = wp_check_filetype( $filepath, $allowed_mime_types ); return in_array( $filetype['type'], $allowed_mime_types, true ); diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index 5bfbbb3f3ce6b..494e84c902404 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -177,7 +177,7 @@ private function get_upload_overrides( $filename ) { // 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_expected_font_mime_types_per_php_version(), + 'mimes' => WP_Font_Library::get_font_mime_types(), 'unique_filename_callback' => static function () use ( $filename ) { // Keep the original filename. return $filename; diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index c35db26bbe00d..0a5ce2c3dce71 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -36,7 +36,7 @@ class WP_Font_Library { * * @return Array A collection of mime types keyed by file extension. */ - public static function get_expected_font_mime_types_per_php_version( $php_version_id = PHP_VERSION_ID ) { + public static function get_font_mime_types( $php_version_id = PHP_VERSION_ID ) { $php_7_ttf_mime_type = $php_version_id >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; return array( @@ -139,6 +139,6 @@ public static function set_upload_dir( $defaults ) { * @return array Modified upload directory. */ public static function set_allowed_mime_types( $mime_types ) { - return array_merge( $mime_types, self::get_expected_font_mime_types_per_php_version() ); + return array_merge( $mime_types, self::get_font_mime_types() ); } } diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 3b75cde941cc9..66be20274fb78 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -1,6 +1,6 @@ assertEquals( $mimes, $expected ); } From f38bb5963b4a5a31bab31e8801f322988d43a2ce Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 15:53:01 -0300 Subject: [PATCH 057/105] make string translatable Co-authored-by: Pascal Birchler <841956+swissspidy@users.noreply.github.com> --- src/wp-includes/fonts/class-wp-font-library.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index 0a5ce2c3dce71..8217bae8ff2b7 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -60,7 +60,7 @@ 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.' ); + return new WP_Error( 'font_collection_registration_error', __( 'Font collection already registered.' ) ); } self::$collections[ $config['id'] ] = $new_collection; From b005321aa9ddb78078fe1a047b14c91829ec6056 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Mon, 2 Oct 2023 20:56:10 +0200 Subject: [PATCH 058/105] Add id argument to the font collection. --- .../endpoints/class-wp-rest-font-library-controller.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 99adc65802a22..60a864eaf5b60 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -98,6 +98,12 @@ public function register_routes() { $this->namespace, '/' . $this->rest_base . '/collections' . '/(?P[\/\w-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'The id of a font collection.' ), + 'type' => 'string', + ), + ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_font_collection' ), From 2b29de4dc1ab799f0b8ec40d3775c0f65ef351d8 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 16:00:31 -0300 Subject: [PATCH 059/105] remove triling comma --- .../endpoints/class-wp-rest-font-library-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 60a864eaf5b60..623ac9e5a874f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -79,7 +79,7 @@ public function register_routes() { ), ), ), - ), + ) ); register_rest_route( From aaf3741cb00737bd62b41bba54381338d9eedc32 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 2 Oct 2023 16:06:32 -0300 Subject: [PATCH 060/105] change WP_REST_Response by rest_ensure_response Co-authored-by: Jonny Harris --- .../endpoints/class-wp-rest-font-library-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 623ac9e5a874f..ab0a2e50af188 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -135,7 +135,7 @@ public function get_font_collection( $request ) { $collection_with_data->add_data( array( 'status' => 500 ) ); return $collection_with_data; } - return new WP_REST_Response( $collection_with_data ); + return rest_ensure_response( $collection_with_data ); } /** From d6534cc08a4262f02c858d59a7e1a1d4c945b0b5 Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 13:52:33 -0500 Subject: [PATCH 061/105] Remove path_join() for constant in tests --- .../fonts/font-library/wpFontFamily/base.php | 2 +- .../font-library/wpFontFamily/install.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php index 2906686c4543a..f4b8e6cd89a1b 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php @@ -36,7 +36,7 @@ public function set_up() { parent::set_up(); $merriweather_tmp_name = wp_tempnam( 'Merriweather-' ); - copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $merriweather_tmp_name ); + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $merriweather_tmp_name ); $this->merriweather = array( 'font_data' => array( 'name' => 'Merriweather', diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php index 1562dd27cf7bf..402b032c1ab68 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php @@ -162,13 +162,13 @@ public function test_should_move_local_fontfaces( $font_data, array $files_data, // Set up the temporary files. foreach ( $files_data as $file ) { if ( 'font/ttf' === $file['type'] ) { - copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); } elseif ( 'font/woff' === $file['type'] ) { - copy( path_join( DIR_TESTDATA, 'fonts/cooper-hewitt.woff' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/cooper-hewitt.woff', $file['tmp_name'] ); } elseif ( 'font/woff2' === $file['type'] ) { - copy( path_join( DIR_TESTDATA, 'fonts/DMSans.woff2' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/DMSans.woff2', $file['tmp_name'] ); } elseif ( 'application/vnd.ms-opentype' === $file['type'] ) { - copy( path_join( DIR_TESTDATA, 'fonts/gilbert-color.otf' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/gilbert-color.otf', $file['tmp_name'] ); } } @@ -343,7 +343,7 @@ public function data_should_move_local_fontfaces() { 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( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); } $font = new WP_Font_Family( $font_data ); @@ -533,16 +533,16 @@ public function test_should_overwrite_fontface_with_different_extension() { // Set up the temporary files. foreach ( $files_data_initial as $file ) { if ( 'font/ttf' === $file['type'] ) { - copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); } elseif ( 'font/woff' === $file['type'] ) { - copy( path_join ( DIR_TESTDATA, 'fonts/cooper-hewitt.woff' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/cooper-hewitt.woff', $file['tmp_name'] ); } } foreach ( $files_data_overwrite as $file ) { if ( 'font/ttf' === $file['type'] ) { - copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/Merriweather.ttf', $file['tmp_name'] ); } elseif ( 'font/woff' === $file['type'] ) { - copy( path_join ( DIR_TESTDATA, 'fonts/cooper-hewitt.woff' ), $file['tmp_name'] ); + copy( DIR_TESTDATA . '/fonts/cooper-hewitt.woff', $file['tmp_name'] ); } } From 3b01b468d533e27fbfeb6d194a7595c282d464fc Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 13:56:50 -0500 Subject: [PATCH 062/105] Change subpackage to Fonts --- src/wp-includes/fonts/class-wp-font-collection.php | 2 +- src/wp-includes/fonts/class-wp-font-family-utils.php | 2 +- src/wp-includes/fonts/class-wp-font-family.php | 2 +- src/wp-includes/fonts/class-wp-font-library.php | 2 +- .../tests/fonts/font-library/wpFontCollection/__construct.php | 2 +- .../tests/fonts/font-library/wpFontCollection/getData.php | 2 +- .../tests/fonts/font-library/wpFontFamily/__construct.php | 2 +- tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php | 2 +- .../phpunit/tests/fonts/font-library/wpFontFamily/getData.php | 2 +- .../tests/fonts/font-library/wpFontFamily/getDataAsJson.php | 2 +- .../tests/fonts/font-library/wpFontFamily/getFontPost.php | 2 +- .../tests/fonts/font-library/wpFontFamily/hasFontFaces.php | 2 +- .../phpunit/tests/fonts/font-library/wpFontFamily/install.php | 2 +- .../tests/fonts/font-library/wpFontFamily/uninstall.php | 2 +- .../font-library/wpFontFamilyUtils/getFilenameFromFontFace.php | 2 +- .../fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php | 2 +- .../fonts/font-library/wpFontFamilyUtils/mergeFontsData.php | 2 +- .../fonts/font-library/wpFontLibrary/getFontCollection.php | 2 +- .../fonts/font-library/wpFontLibrary/getFontCollections.php | 2 +- .../tests/fonts/font-library/wpFontLibrary/getFontsDir.php | 2 +- .../tests/fonts/font-library/wpFontLibrary/getMimeTypes.php | 2 +- .../font-library/wpFontLibrary/registerFontCollection.php | 2 +- .../tests/fonts/font-library/wpFontLibrary/setUploadDir.php | 2 +- .../fonts/font-library/wpRestFontLibraryController/base.php | 2 +- .../wpRestFontLibraryController/getFontCollection.php | 2 +- .../wpRestFontLibraryController/getFontCollections.php | 3 +-- .../font-library/wpRestFontLibraryController/installFonts.php | 3 +-- .../wpRestFontLibraryController/registerRoutes.php | 3 +-- .../wpRestFontLibraryController/uninstallFonts.php | 3 +-- 29 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php index a0c0d7da9d22a..c60dbeefbc4d7 100644 --- a/src/wp-includes/fonts/class-wp-font-collection.php +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -5,7 +5,7 @@ * This file contains the Font Collection class definition. * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * @since 6.4.0 */ diff --git a/src/wp-includes/fonts/class-wp-font-family-utils.php b/src/wp-includes/fonts/class-wp-font-family-utils.php index e8811769783b9..538c35fb6120e 100644 --- a/src/wp-includes/fonts/class-wp-font-family-utils.php +++ b/src/wp-includes/fonts/class-wp-font-family-utils.php @@ -5,7 +5,7 @@ * This file contains utils fot Font Family class. * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * @since 6.4.0 */ diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index 494e84c902404..27a390d023d85 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -5,7 +5,7 @@ * This file contains the Font Family class definition. * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * @since 6.4.0 */ diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index 8217bae8ff2b7..2c79bb0d8ea4b 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -5,7 +5,7 @@ * This file contains the Font Library class definition. * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * @since 6.4.0 */ diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php index 5c2b7b5c02793..a55ab9aaa643f 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -3,7 +3,7 @@ * Test WP_Font_Collection::__construct(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php index 4e3b1cd78b0a9..9f843205c3a48 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php @@ -3,7 +3,7 @@ * Test WP_Font_Collection::get_data(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php index 3a1e387c3651b..c3bd5790956b9 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/__construct.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::__construct(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php index f4b8e6cd89a1b..2dd5cd9f42afc 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/base.php @@ -3,7 +3,7 @@ * Test Case for WP_Font_Family tests. * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts */ abstract class WP_Font_Family_UnitTestCase extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php index 2de88ad3aae37..137261de85aa3 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getData.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::get_data(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php index b18b2d0b4f4b3..2c779bff0876f 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getDataAsJson.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::get_data_as_json(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php index 0d424e0c153d6..7cdb90ec47ca0 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::get_font_post(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts */ require_once __DIR__ . '/base.php'; diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php index 0c153d62aa79d..e493a99334bd8 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/hasFontFaces.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::has_font_faces(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php index 402b032c1ab68..cc20557ee9c87 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::install(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts */ require_once __DIR__ . '/base.php'; diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php index ded822346f970..f73ba7a6ffe9a 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php @@ -3,7 +3,7 @@ * Test WP_Font_Family::uninstall(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts */ require_once __DIR__ . '/base.php'; diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php index 0bd5b47ea2378..7503f4d3571d5 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/getFilenameFromFontFace.php @@ -3,7 +3,7 @@ * Test WP_Font_Family_Utils::get_filename_from_font_face(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php index e30c199612b8a..17d6488179935 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php @@ -3,7 +3,7 @@ * Test WP_Font_Family_Utils::has_font_mime_type(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php index 21517a0970f93..c1717b5f6c8f3 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php @@ -3,7 +3,7 @@ * Test WP_Font_Family_Utils::merge_fonts_data(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php index bfdb7258fa11a..5aabaf66dd305 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -3,7 +3,7 @@ * Test WP_Font_Library::get_font_collections(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php index 97e66e64e8716..dbe6e0d11209d 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -3,7 +3,7 @@ * Test WP_Font_Library::get_font_collections(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php index 4bbafc55a2147..ae2c208bdac9b 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php @@ -3,7 +3,7 @@ * Test WP_Font_Library::get_fonts_dir(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 66be20274fb78..149961e3e5794 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -3,7 +3,7 @@ * Test WP_Font_Family_Utils::get_font_mime_types(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index 6bc5fbb8161ce..8475c183099be 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -3,7 +3,7 @@ * Test WP_Font_Library::register_font_collection(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php index daa4c84aad900..a11eaec152e68 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php @@ -3,7 +3,7 @@ * Test WP_Font_Library::set_upload_dir(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php index 99e1d01cad73b..cebd4af33ebbe 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php @@ -3,7 +3,7 @@ * Test Case for WP_REST_Font_Library_Controller tests. * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts */ abstract class WP_REST_Font_Library_Controller_UnitTestCase extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php index f1a0a6a0cd510..97ff9c7e96911 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php @@ -3,7 +3,7 @@ * Test WP_REST_Font_Library_Controller::get_font_collection(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php index ad120ee36fce4..79742e4d7d8d5 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php @@ -3,14 +3,13 @@ * Test WP_REST_Font_Library_Controller::get_font_collections(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library * * @covers WP_REST_Font_Library_Controller::get_font_collections */ - class Tests_Fonts_WPRESTFontLibraryController_GetFontCollections extends WP_REST_Font_Library_Controller_UnitTestCase { public function test_get_font_collections_with_no_collection_registered() { diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php index 9dabd3491a27e..4edc5e08908fa 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php @@ -3,14 +3,13 @@ * Test WP_REST_Font_Library_Controller::install_fonts(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library * * @covers WP_REST_Font_Library_Controller::install_fonts */ - class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_Library_Controller_UnitTestCase { /** diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php index 2ac7b93c3a414..8bc9efd0fc52f 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php @@ -3,14 +3,13 @@ * Test WP_REST_Font_Library_Controller::register_routes(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library * * @covers WP_REST_Font_Library_Controller::register_routes */ - class Tests_Fonts_WPRESTFontLibraryController_RegisterRoutes extends WP_UnitTestCase { public function test_register_routes() { diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php index a3b613e6f983e..a45e910b27473 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php @@ -3,14 +3,13 @@ * Test WP_REST_Font_Library_Controller::install_fonts(). * * @package WordPress - * @subpackage Font Library + * @subpackage Fonts * * @group fonts * @group font-library * * @covers WP_REST_Font_Library_Controller::install_fonts */ - class Tests_Fonts_WPRESTFontLibraryController_UninstallFonts extends WP_REST_Font_Library_Controller_UnitTestCase { /** From f16ead0660890f77c41c6a188f6bbc1302d30674 Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 13:58:44 -0500 Subject: [PATCH 063/105] Initialize properties to empty array to avoid a potential `null` value. --- src/wp-includes/fonts/class-wp-font-collection.php | 2 +- src/wp-includes/fonts/class-wp-font-family.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php index c60dbeefbc4d7..ac11ddba77e03 100644 --- a/src/wp-includes/fonts/class-wp-font-collection.php +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -23,7 +23,7 @@ class WP_Font_Collection { * * @var array */ - private $config; + private $config = array(); /** * WP_Font_Collection constructor. diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index 27a390d023d85..42c4d8728fcbd 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -23,7 +23,7 @@ class WP_Font_Family { * * @var array */ - private $data; + private $data = array(); /** * WP_Font_Family constructor. From 30369c60e67dc40da4ecb345b766fd2dd1beff2b Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 14:05:30 -0500 Subject: [PATCH 064/105] Empty line before end of function return. for consistency in Core. --- src/wp-includes/fonts/class-wp-font-collection.php | 1 + src/wp-includes/fonts/class-wp-font-family.php | 3 +++ src/wp-includes/fonts/class-wp-font-library.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php index ac11ddba77e03..848a4fd83cbe7 100644 --- a/src/wp-includes/fonts/class-wp-font-collection.php +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -103,6 +103,7 @@ public function get_data() { $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.php b/src/wp-includes/fonts/class-wp-font-family.php index 42c4d8728fcbd..cdc7d43f4e7b8 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -89,6 +89,7 @@ private function remove_font_family_assets() { } } } + return true; } @@ -156,6 +157,7 @@ private static function delete_font_face_assets( $font_face ) { return false; } } + return true; } @@ -512,6 +514,7 @@ private function get_intersecting_font_faces( $existing, $incoming ) { } } } + return $intersecting; } diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index 2c79bb0d8ea4b..01bc309ab760d 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -64,6 +64,7 @@ public static function register_font_collection( $config ) { } self::$collections[ $config['id'] ] = $new_collection; + return $new_collection; } @@ -90,6 +91,7 @@ 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.' ); } From e1f0b58b25cce07ecf80451d29196027cc8f0ce1 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Mon, 2 Oct 2023 21:43:39 +0200 Subject: [PATCH 065/105] 1. Implement schema. 2. Mark the ID parameter as required. 3. Remove unnecessary concatenation. --- .../class-wp-rest-font-library-controller.php | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index ab0a2e50af188..19c6dd7d64a09 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -96,12 +96,13 @@ public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->rest_base . '/collections' . '/(?P[\/\w-]+)', + '/' . $this->rest_base . '/collections/(?P[\/\w-]+)', array( 'args' => array( 'id' => array( 'description' => __( 'The id of a font collection.' ), 'type' => 'string', + 'required' => true, ), ), array( @@ -109,6 +110,7 @@ public function register_routes() { 'callback' => array( $this, 'get_font_collection' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } @@ -335,6 +337,112 @@ public function update_font_library_permissions_check() { return true; } + /** + * 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( + 'fontFamilies' => array( + 'description' => __( 'List of font families.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( 'Name of the font family.' ), + 'type' => 'string', + ), + 'fontFamily' => 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', + ), + 'fontFace' => array( + 'description' => __( 'Font face details.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'downloadFromUrl' => array( + 'description' => __( 'URL to download the font.' ), + 'type' => 'string', + ), + 'fontWeight' => array( + 'description' => __( 'Font weight.' ), + 'type' => 'string', + ), + 'fontStyle' => array( + 'description' => __( 'Font style.' ), + 'type' => 'string', + ), + 'fontFamily' => 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 ); + } + /** * Checks whether the user has write permissions to the temp and fonts directories. * From aec6c93bb58a4bb1eae8c63cb921d6139b219953 Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 14:59:01 -0500 Subject: [PATCH 066/105] Removes passing PHP version to WP_Font_Library::get_font_mime_types(). The PHP version will not change in production during a web request cycle. The tests identify which PHP version is currently running to return the expected results for that version. As the test suite runs on each of these PHP versions, this new tests design avoids passing the version argument while actually testing the expected results for the currently running PHP version. --- .../fonts/class-wp-font-library.php | 19 +-- .../wpFontLibrary/getMimeTypes.php | 108 +++++++----------- 2 files changed, 52 insertions(+), 75 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php index 01bc309ab760d..91cab9215e078 100644 --- a/src/wp-includes/fonts/class-wp-font-library.php +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -26,24 +26,25 @@ class WP_Font_Library { private static $collections = array(); /** - * Provides the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. + * Provides the expected mime-type value for font files per-PHP release. * - * This is necessary until a collection of valid mime-types per-file extension can be provided to 'upload_mimes' filter. + * Due to differences in the values returned these values differ between PHP versions. * - * @since 6.4.0 + * This is necessary until a collection of valid mime-types per-file extension can be + * provided to 'upload_mimes' filter. * - * @param array $php_version_id The version of PHP to provide mime types for. The default is the current PHP version. + * @since 6.4.0 * * @return Array A collection of mime types keyed by file extension. */ - public static function get_font_mime_types( $php_version_id = PHP_VERSION_ID ) { - $php_7_ttf_mime_type = $php_version_id >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; + public static function get_font_mime_types() { + $php_7_ttf_mime_type = PHP_VERSION_ID >= 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', + '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', ); } diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 149961e3e5794..a1963e5a199d6 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -12,78 +12,54 @@ */ class Tests_Fonts_WpFontsFamilyUtils_GetMimeTypes extends WP_UnitTestCase { - /** - * @dataProvider data_should_supply_correct_mime_type_for_php_version - * - * @param array $php_version_id PHP_VERSION_ID value. - * @param array $expected Expected mime types. - */ - public function test_should_supply_correct_mime_type_for_php_version( $php_version_id, $expected ) { - $mimes = WP_Font_Library::get_font_mime_types( $php_version_id ); - $this->assertEquals( $mimes, $expected ); + public function test_should_supply_correct_mime_type_for_the_running_php_version() { + $mimes = WP_Font_Library::get_font_mime_types(); + $expected = $this->get_expected_mime_for_tests_php_version(); + $this->assertSame( $mimes, $expected ); } /** - * Data provider. + * Get the expected results for the running PHP version. * - * @return array[] + * @return string[] */ - public function data_should_supply_correct_mime_type_for_php_version() { + 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( - 'version 7.2' => array( - 'php_version_id' => 70200, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'application/x-font-ttf', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 7.3' => array( - 'php_version_id' => 70300, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'application/font-sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 7.4' => array( - 'php_version_id' => 70400, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 8.0' => array( - 'php_version_id' => 80000, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 8.1' => array( - 'php_version_id' => 80100, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - ), - ), - 'version 8.2' => array( - 'php_version_id' => 80200, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - ), - ), + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => 'font/sfnt', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', ); } } From b34fe52a2e9a4948f5f6caebc34f7db73e182286 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Mon, 2 Oct 2023 23:09:45 +0200 Subject: [PATCH 067/105] Implement the schema for the font collections endpoint. --- .../class-wp-rest-font-library-controller.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 19c6dd7d64a09..49b73462e51d2 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -91,6 +91,7 @@ public function register_routes() { 'callback' => array( $this, 'get_font_collections' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), + 'schema' => array( $this, 'get_font_collections_schema' ), ) ); @@ -337,6 +338,44 @@ public function update_font_library_permissions_check() { 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_font_collections_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'font-collections', + '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' => __( 'Link to the list of font families.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ); + } + /** * Retrieves the item's schema, conforming to JSON Schema. * From e948c1b764370bba2fa982b190e3c9dbb179cfab Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 16:39:08 -0500 Subject: [PATCH 068/105] Multiline comments props costdev --- .../fonts/class-wp-font-family.php | 66 +++++++++++++------ .../class-wp-rest-font-library-controller.php | 1 + 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index cdc7d43f4e7b8..2af27be09b263 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -171,13 +171,19 @@ private static function delete_font_face_assets( $font_face ) { */ private function get_upload_overrides( $filename ) { return array( - // Arbitrary string to avoid the is_uploaded_file() check applied - // when using 'wp_handle_upload'. + + /* + * 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(). + + /* + * 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 ) { @@ -231,8 +237,10 @@ private function download_asset( $url, $filename ) { return false; } - // Returns the relative path to the downloaded font asset to be used as - // font face src. + /* + * Returns the relative path to the downloaded font asset to be used as + * font face 'src'. + */ return $handled_file['url']; } @@ -256,12 +264,16 @@ private function move_font_face_asset( $font_face, $file ) { $file['name'] ); - // Remove the uploaded font asset reference from the font face definition - // because it is no longer needed. + /* + * 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 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; } @@ -276,8 +288,11 @@ private function move_font_face_asset( $font_face, $file ) { $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. + + /* + * If the file was successfully moved, update the font face definition + * to reference the new file location. + */ $new_font_face['src'] = $handled_file['url']; } @@ -301,14 +316,18 @@ private function sanitize() { ), ), ); - // Creates a new WP_Theme_JSON object with the new fonts to - // leverage sanitization and validation. + + /* + * 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; } @@ -349,8 +368,10 @@ private function download_font_face_assets( $font_face ) { $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. + /* + * 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; @@ -373,8 +394,11 @@ private function download_or_move_font_faces( $files ) { $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). + + /* + * 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; @@ -396,8 +420,10 @@ private function download_or_move_font_faces( $files ) { $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 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, diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 49b73462e51d2..37d9b88245372 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -138,6 +138,7 @@ public function get_font_collection( $request ) { $collection_with_data->add_data( array( 'status' => 500 ) ); return $collection_with_data; } + return rest_ensure_response( $collection_with_data ); } From 0926426656e4170b470f32677209580db396964f Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Tue, 3 Oct 2023 00:03:49 +0200 Subject: [PATCH 069/105] Update wp-api-generated.js. --- tests/qunit/fixtures/wp-api-generated.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 4af88d2ff930a..8e8b77ed5803a 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12232,7 +12232,13 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], - "args": [] + "args": { + "id": { + "description": "The id of a font collection.", + "type": "string", + "required": true + } + } } ] } From 11b26dbbaa421adedee8066a004dcc40d0f3689e Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 17:12:07 -0500 Subject: [PATCH 070/105] PHPCS: Remove extra empty line at end of file --- src/wp-includes/fonts.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/fonts.php b/src/wp-includes/fonts.php index 4bd810a52ebb9..a32195efef4e2 100644 --- a/src/wp-includes/fonts.php +++ b/src/wp-includes/fonts.php @@ -85,4 +85,3 @@ function wp_register_default_font_collection() { ) ); } - From feb11999f753c808a18dd44a9561f74e91bf009a Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Mon, 2 Oct 2023 17:35:49 -0500 Subject: [PATCH 071/105] Remove isset() where empty() is also used. Prop costdev --- src/wp-includes/fonts/class-wp-font-family.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index 2af27be09b263..e8a54d054680a 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -555,11 +555,11 @@ private function get_intersecting_font_faces( $existing, $incoming ) { private function update_font_post( $post ) { $post_font_data = json_decode( $post->post_content, true ); $new_data = WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data ); - if ( isset( $post_font_data['fontFace'] ) && ! empty( $post_font_data['fontFace'] ) ) { + if ( ! empty( $post_font_data['fontFace'] ) ) { $intersecting = $this->get_intersecting_font_faces( $post_font_data['fontFace'], $new_data['fontFace'] ); } - if ( isset( $intersecting ) && ! empty( $intersecting ) ) { + if ( ! empty( $intersecting ) ) { $serialized_font_faces = array_map( 'serialize', $new_data['fontFace'] ); $serialized_intersecting = array_map( 'serialize', $intersecting ); From a776cee70c12a55d3184a73652e86112f610dcb9 Mon Sep 17 00:00:00 2001 From: Tonya Mork Date: Mon, 2 Oct 2023 19:14:03 -0500 Subject: [PATCH 072/105] Use $request['id'] - props spacedmonkey Co-authored-by: Jonny Harris --- .../endpoints/class-wp-rest-font-library-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 37d9b88245372..5c50a8003baf2 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -125,7 +125,7 @@ public function register_routes() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_font_collection( $request ) { - $id = $request->get_param( 'id' ); + $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 ) ) { From 909da307f48092c1f250e5c6d3ee38460a70c05d Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Tue, 3 Oct 2023 15:38:18 -0300 Subject: [PATCH 073/105] make install and uninstall endpoints singular. Install response is not a WP_Post intead of json data. Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com> Co-authored-by: Jeffrey Pearce <937325+jeffikus@users.noreply.github.com> --- .../fonts/class-wp-font-family.php | 121 ++++--- .../class-wp-rest-font-library-controller.php | 309 ++++++------------ 2 files changed, 174 insertions(+), 256 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index e8a54d054680a..f5c72decb1962 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -33,10 +33,18 @@ class WP_Font_Family { * @param array $font_data Font family data. * @throws Exception If the font family data is missing the slug. */ - public function __construct( $font_data = array() ) { - if ( empty( $font_data['slug'] ) ) { - throw new Exception( 'Font family data is missing the slug.' ); - } + public function __construct( $id = null ) { + $this->id = $id; + } + + /** + * Sets the font family data. + * + * @since 6.4.0 + * + * @param array $font_data An array in fontFamily theme.json format. + */ + public function set_data( $font_data ) { $this->data = $font_data; } @@ -101,17 +109,15 @@ private function remove_font_family_assets() { * @return bool|WP_Error True if the font family was uninstalled, WP_Error otherwise. */ public function uninstall() { - $post = $this->get_data_from_post(); - if ( null === $post ) { - return new WP_Error( - 'font_family_not_found', - __( 'The font family could not be found.' ) - ); + $data = $this->populate_data_from_post_content_by_id(); + + if ( is_wp_error( $data ) ) { + return $data; } if ( ! $this->remove_font_family_assets() || - ! wp_delete_post( $post->ID, true ) + ! wp_delete_post( $this->id, true ) ) { return new WP_Error( 'font_family_not_deleted', @@ -450,47 +456,54 @@ private function download_or_move_font_faces( $files ) { } /** - * Gets the post for a font family. + * Populates the data for this object from the database using a post id. * * @since 6.4.0 * - * @return WP_Post|null The post for this font family object or - * null if the post does not exist. + * @return bool|WP_Error True if the data was populated, WP_Error otherwise. */ - public function get_font_post() { - $args = array( - 'post_type' => 'wp_font_family', - 'post_name' => $this->data['slug'], - 'name' => $this->data['slug'], - 'posts_per_page' => 1, - ); - - $posts_query = new WP_Query( $args ); - - if ( $posts_query->have_posts() ) { - return $posts_query->posts[0]; + private function populate_data_from_post_content_by_id() { + $post = get_post( $this->id ); + if ( $post && !empty( $post ) ) { + $post_content_data = json_decode( $post->post_content, true ); + // If the post content is not valid JSON, return null. + if ( is_null( $post_content_data ) ) { + return new WP_Error( + 'font_family_post_content_not_valid_json', + __( 'The font family post content is not valid JSON.' ) + ); + } + $this->set_data( $post_content_data ); + return true; } - - return null; + return new WP_Error( + 'font_family_not_found', + __( 'The font family could not be found.' ) + ); } /** - * Gets the data for this object from the database and - * sets it to the data property. + * Gets a post for a font family by its slug. * * @since 6.4.0 * - * @return WP_Post|null The post for this font family object or - * null if the post does not exist. + * @return WP_Post|null The post if the post exists, null otherwise. */ - private function get_data_from_post() { - $post = $this->get_font_post(); - if ( $post ) { - $this->data = json_decode( $post->post_content, true ); - return $post; + private 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; } - return null; + $post = $posts_query->posts[0]; + return $post; } /** @@ -498,7 +511,7 @@ private function get_data_from_post() { * * @since 6.4.0 * - * @return int|WP_Error Post ID if the post was created, WP_Error otherwise. + * @return bool|WP_Error True if the post was created, WP_Error otherwise. */ private function create_font_post() { $post = array( @@ -516,8 +529,8 @@ private function create_font_post() { __( 'Font post creation failed.' ) ); } - - return $post_id; + $this->id = $post_id; + return true; } /** @@ -550,7 +563,7 @@ private function get_intersecting_font_faces( $existing, $incoming ) { * @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. + * @return bool|WP_Error True if the update was successful, WP_Error otherwise. */ private function update_font_post( $post ) { $post_font_data = json_decode( $post->post_content, true ); @@ -587,7 +600,7 @@ private function update_font_post( $post ) { ); } - return $post_id; + return true; } /** @@ -602,12 +615,15 @@ private function update_font_post( $post ) { private function create_or_update_font_post() { $this->sanitize(); - $post = $this->get_font_post(); - if ( $post ) { + $post = $this->get_post_by_slug(); + + if( $post ){ + $this->id = $post->ID; return $this->update_font_post( $post ); } - + return $this->create_font_post(); + } /** @@ -616,7 +632,7 @@ private function create_or_update_font_post() { * @since 6.4.0 * * @param array $files Optional. An array of files to be installed. Default null. - * @return array|WP_Error An array of font family data on success, WP_Error otherwise. + * @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' ) ); @@ -632,12 +648,11 @@ public function install( $files = null ) { ); } - $post_id = $this->create_or_update_font_post(); - - if ( is_wp_error( $post_id ) ) { - return $post_id; + $result = $this->create_or_update_font_post(); + if ( is_wp_error( $result ) ) { + return $result; } - - return $this->get_data(); + $post = get_post( $this->id ); + return rest_ensure_response( $post ); } } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php index 5c50a8003baf2..f056c9e49a316 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php @@ -22,7 +22,7 @@ class WP_REST_Font_Library_Controller extends WP_REST_Controller { * @since 6.4.0 */ public function __construct() { - $this->rest_base = 'fonts'; + $this->rest_base = 'font-family'; $this->namespace = 'wp/v2'; } @@ -38,46 +38,39 @@ public function register_routes() { array( array( 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'install_fonts' ), + 'callback' => array( $this, 'install_font' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => array( - 'font_families' => array( - 'required' => true, - 'type' => 'string', - 'validate_callback' => array( $this, 'validate_install_font_families' ), + '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' ), + ), + ) ), ) ); register_rest_route( $this->namespace, - '/' . $this->rest_base, + '/' . $this->rest_base . '/(?P[\/\w-]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'uninstall_fonts' ), + 'callback' => array( $this, 'uninstall_font' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => array( - 'font_families' => array( - 'type' => 'array', - 'description' => __( 'The font families to uninstall.' ), - 'required' => true, - 'minItems' => 1, - 'items' => array( - 'required' => true, - 'type' => 'object', - 'properties' => array( - 'slug' => array( - 'type' => 'string', - 'description' => __( 'The font family slug.' ), - 'required' => true, - ), - ), - ), - ), - ), ), ) ); @@ -159,99 +152,59 @@ public function get_font_collections() { } /** - * Returns validation errors in font families data for installation. + * Returns validation errors in font faces data for installation. * * @since 6.4.0 * - * @param array[] $font_families Font families to install. + * @param array[] $font_faces Font faces to install. * @param array $files Files to install. - * @return array Array of error messages. + * @return WP_Error Validation errors. */ - private function get_validation_errors( $font_families, $files ) { - $error_messages = array(); + private function get_validation_errors( $font_faces, $files ) { + $error = new WP_Error(); - if ( ! is_array( $font_families ) ) { - $error_messages[] = __( 'font_families should be an array of font families.' ); - return $error_messages; + if ( ! is_array( $font_faces ) ) { + $error->add( 'rest_invalid_param', __( 'fontFace should be an array.' ) ); + return $error; } - // Checks if there is at least one font family. - if ( count( $font_families ) < 1 ) { - $error_messages[] = __( 'font_families should have at least one font family definition.' ); - return $error_messages; + if ( count( $font_faces ) < 1 ) { + $error->add( 'rest_invalid_param', __( 'fontFace should have at least one item.' ) ); + return $error; } - for ( $family_index = 0; $family_index < count( $font_families ); $family_index++ ) { - $font_family = $font_families[ $family_index ]; - - if ( - ! isset( $font_family['slug'] ) || - ! isset( $font_family['name'] ) || - ! isset( $font_family['fontFamily'] ) - ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have slug, name and fontFamily properties defined.' ), - $family_index + 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_family['fontFace'] ) ) { - if ( ! is_array( $font_family['fontFace'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have fontFace property defined as an array.' ), - $family_index - ); - continue; - } + 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 ( count( $font_family['fontFace'] ) < 1 ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have at least one font face definition.' ), - $family_index + 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 ); - } - - if ( ! empty( $font_family['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $font_family['fontFace'] ); $face_index++ ) { - - $font_face = $font_family['fontFace'][ $face_index ]; - if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.' ), - $family_index, - $face_index - ); - } - - if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.' ), - $family_index, - $face_index - ); - } - - if ( isset( $font_face['uploadedFile'] ) ) { - if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.' ), - $family_index, - $face_index - ); - } - } - } + $error->add( 'rest_invalid_param', $error_message ); } } } - - return $error_messages; + return $error; } /** @@ -259,20 +212,29 @@ private function get_validation_errors( $font_families, $files ) { * * @since 6.4.0 * - * @param string $param The font families to install. + * @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_install_font_families( $param, $request ) { - $font_families = json_decode( $param, true ); - $files = $request->get_file_params(); - $error_messages = $this->get_validation_errors( $font_families, $files ); + 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 ( empty( $error_messages ) ) { - return true; + if ( $validation->has_errors() ) { + $validation->add_data( array( 'status' => 400 ) ); + return $validation; } - return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) ); + return true; } /** @@ -283,40 +245,13 @@ public function validate_install_font_families( $param, $request ) { * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function uninstall_fonts( $request ) { - $fonts_to_uninstall = $request->get_param( 'font_families' ); - - $errors = array(); - $successes = array(); - - if ( empty( $fonts_to_uninstall ) ) { - $errors[] = new WP_Error( - 'no_fonts_to_install', - __( 'No fonts to uninstall' ) - ); - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - $response = rest_ensure_response( $data ); - $response->set_status( 400 ); - return $response; - } - - foreach ( $fonts_to_uninstall as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $result = $font->uninstall(); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } + public function uninstall_font( $request ) { + $font_family = new WP_Font_Family( $request['id'] ); + $result = $font_family->uninstall(); + if ( is_wp_error( $result ) ) { + $result->add_data( array( 'status' => 500 ) ); } - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - return rest_ensure_response( $data ); + return rest_ensure_response( $result ); } /** @@ -505,17 +440,15 @@ private function has_write_permission() { * * @since 6.4.0 * - * @param array[] $font_families Font families to install. + * @param array $font_family Font family definition. * @return bool Whether the request needs write permissions. */ - private function needs_write_permission( $font_families ) { - foreach ( $font_families as $font ) { - if ( isset( $font['fontFace'] ) ) { - foreach ( $font['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; - } + 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; } } } @@ -534,63 +467,33 @@ private function needs_write_permission( $font_families ) { * in the request parameters. * @return WP_REST_Response|WP_Error The updated Font Library post content. */ - public function install_fonts( $request ) { - // Get new fonts to install. - $fonts_param = $request->get_param( 'font_families' ); - - /* - * As this is receiving form data, the font families are encoded as a string. - * The form data is used because local fonts need to use that format to - * attach the files in the request. - */ - $fonts_to_install = json_decode( $fonts_param, true ); - - $successes = array(); - $errors = array(); - $response_status = 200; - - if ( empty( $fonts_to_install ) ) { - $errors[] = new WP_Error( - 'no_fonts_to_install', - __( 'No fonts to install' ) - ); - $response_status = 400; - } + public function install_font( $request ) { + $font_family_data = array ( + 'slug' => $request->get_param( 'slug' ), + 'name' => $request->get_param( 'name' ), + 'fontFamily' => $request->get_param( 'fontFamily' ), + ); - if ( $this->needs_write_permission( $fonts_to_install ) && ! $this->has_write_permission() ) { - $errors[] = new WP_Error( - 'cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.' ) - ); - $response_status = 500; + if ( $request->get_param( 'fontFace' ) ) { + $font_family_data['fontFace'] = json_decode( $request->get_param( 'fontFace' ), true ); } - if ( ! empty( $errors ) ) { - $data = array( - 'successes' => $successes, - 'errors' => $errors, + 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, + ) ); - $response = rest_ensure_response( $data ); - $response->set_status( $response_status ); - return $response; } // Get uploaded files (used when installing local fonts). $files = $request->get_file_params(); - foreach ( $fonts_to_install as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $result = $font->install( $files ); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } - } + $font_family = new WP_Font_Family(); + $font_family->set_data( $font_family_data ); + $result = $font_family->install( $files ); - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - return rest_ensure_response( $data ); + return rest_ensure_response( $result ); } } From fd706d10a7ede80f36121eb30e435cb83b1e7d00 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Tue, 3 Oct 2023 18:54:57 -0300 Subject: [PATCH 074/105] rename font library enpoints and split font library controller in 2 different classes. --- src/wp-includes/rest-api.php | 8 +- ...ss-wp-rest-font-collections-controller.php | 275 ++++++++++++++++++ ...lass-wp-rest-font-families-controller.php} | 216 +------------- src/wp-settings.php | 3 +- 4 files changed, 288 insertions(+), 214 deletions(-) create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php rename src/wp-includes/rest-api/endpoints/{class-wp-rest-font-library-controller.php => class-wp-rest-font-families-controller.php} (55%) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index c8c63be7e68a4..5e22e4f3b7e99 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -386,8 +386,12 @@ function create_initial_rest_routes() { $controller = new WP_REST_Navigation_Fallback_Controller(); $controller->register_routes(); - // Font Library. - $controller = new WP_REST_Font_Library_Controller(); + // 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..55944aa8e5976 --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php @@ -0,0 +1,275 @@ +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_font_collections' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + 'schema' => array( $this, 'get_font_collections_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\/\w-]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'The id of a font collection.' ), + 'type' => 'string', + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_font_collection' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_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_font_collection( $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; + } + + return rest_ensure_response( $collection_with_data ); + } + + /** + * Gets the font collections available. + * + * @since 6.4.0 + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_font_collections() { + $collections = array(); + foreach ( WP_Font_Library::get_font_collections() as $collection ) { + $collections[] = $collection->get_config(); + } + + 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_font_collections_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'font-collections', + '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' => __( 'Link to the list of font families.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ); + } + + /** + * 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( + 'fontFamilies' => array( + 'description' => __( 'List of font families.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( 'Name of the font family.' ), + 'type' => 'string', + ), + 'fontFamily' => 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', + ), + 'fontFace' => array( + 'description' => __( 'Font face details.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'downloadFromUrl' => array( + 'description' => __( 'URL to download the font.' ), + 'type' => 'string', + ), + 'fontWeight' => array( + 'description' => __( 'Font weight.' ), + 'type' => 'string', + ), + 'fontStyle' => array( + 'description' => __( 'Font style.' ), + 'type' => 'string', + ), + 'fontFamily' => 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 ); + } + +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php similarity index 55% rename from src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php rename to src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php index f056c9e49a316..b1f21d8ddd65a 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-library-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php @@ -1,8 +1,8 @@ rest_base = 'font-family'; + $this->rest_base = 'font-families'; $this->namespace = 'wp/v2'; } @@ -87,68 +87,6 @@ public function register_routes() { 'schema' => array( $this, 'get_font_collections_schema' ), ) ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/collections/(?P[\/\w-]+)', - array( - 'args' => array( - 'id' => array( - 'description' => __( 'The id of a font collection.' ), - 'type' => 'string', - 'required' => true, - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collection' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - ), - 'schema' => array( $this, 'get_public_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_font_collection( $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; - } - - return rest_ensure_response( $collection_with_data ); - } - - /** - * Gets the font collections available. - * - * @since 6.4.0 - * - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_font_collections() { - $collections = array(); - foreach ( WP_Font_Library::get_font_collections() as $collection ) { - $collections[] = $collection->get_config(); - } - - return new WP_REST_Response( $collections, 200 ); } /** @@ -274,150 +212,6 @@ public function update_font_library_permissions_check() { 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_font_collections_schema() { - return array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'font-collections', - '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' => __( 'Link to the list of font families.' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - ), - ); - } - - /** - * 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( - 'fontFamilies' => array( - 'description' => __( 'List of font families.' ), - 'type' => 'array', - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'description' => __( 'Name of the font family.' ), - 'type' => 'string', - ), - 'fontFamily' => 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', - ), - 'fontFace' => array( - 'description' => __( 'Font face details.' ), - 'type' => 'array', - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'downloadFromUrl' => array( - 'description' => __( 'URL to download the font.' ), - 'type' => 'string', - ), - 'fontWeight' => array( - 'description' => __( 'Font weight.' ), - 'type' => 'string', - ), - 'fontStyle' => array( - 'description' => __( 'Font style.' ), - 'type' => 'string', - ), - 'fontFamily' => 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 ); - } - /** * Checks whether the user has write permissions to the temp and fonts directories. * diff --git a/src/wp-settings.php b/src/wp-settings.php index 86c3b525fd312..d5a7045d86461 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -301,7 +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-library-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'; From 70819418e0fb4ba909b2181a168186c4b5814620 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Tue, 3 Oct 2023 23:57:17 -0300 Subject: [PATCH 075/105] remove unwanted enpoint from WP_REST_Font_Families_Controller class --- .../class-wp-rest-font-families-controller.php | 13 ------------- 1 file changed, 13 deletions(-) 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 index b1f21d8ddd65a..b6814709cd472 100644 --- 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 @@ -74,19 +74,6 @@ public function register_routes() { ), ) ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/collections', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collections' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - ), - 'schema' => array( $this, 'get_font_collections_schema' ), - ) - ); } /** From b49da9a5c350fc3632171636281ace9f29a52e62 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Tue, 3 Oct 2023 23:59:16 -0300 Subject: [PATCH 076/105] dont show custom post type in rest endpoints --- src/wp-includes/post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index fb786c69579c3..d5f85acf058fa 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -567,7 +567,7 @@ function create_initial_post_types() { '_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' => true, + 'show_in_rest' => false, 'rewrite' => false, 'hierarchical' => false, 'capabilities' => array( From 2d369aa99f0149f99f9174804881b3b86a7435d1 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 08:17:32 -0300 Subject: [PATCH 077/105] use lowercase in error message Co-authored-by: Pascal Birchler --- src/wp-includes/fonts/class-wp-font-collection.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php index 848a4fd83cbe7..c8cdb7879a68d 100644 --- a/src/wp-includes/fonts/class-wp-font-collection.php +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -77,26 +77,26 @@ 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.' ) ); + 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.' ) ); + 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.' ) ); + 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.' ) ); + 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.' ) ); + return new WP_Error( 'font_collection_read_error', __( 'Error reading the font collection data JSON file contents.' ) ); } } From 86dab9e1e417323cf0ebe495191ec37385bd6759 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 08:17:54 -0300 Subject: [PATCH 078/105] update wording Co-authored-by: Pascal Birchler --- .../endpoints/class-wp-rest-font-collections-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 55944aa8e5976..76590210ca42a 100644 --- 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 @@ -51,7 +51,7 @@ public function register_routes() { array( 'args' => array( 'id' => array( - 'description' => __( 'The id of a font collection.' ), + 'description' => __( 'Unique identifier for the post.' ), 'type' => 'string', 'required' => true, ), From b167d7f4c17bce42a5bb2c0ad63ba8a607b8b0f6 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 11:24:14 -0300 Subject: [PATCH 079/105] Implementing get_item and get_items for /font-families/ endpoints Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> --- .../fonts/class-wp-font-family.php | 9 +- ...class-wp-rest-font-families-controller.php | 106 ++++++++++++++++-- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index f5c72decb1962..fc50484f6e0aa 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -567,7 +567,12 @@ private function get_intersecting_font_faces( $existing, $incoming ) { */ private function update_font_post( $post ) { $post_font_data = json_decode( $post->post_content, true ); - $new_data = WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data ); + + // 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'] ); } @@ -653,6 +658,6 @@ public function install( $files = null ) { return $result; } $post = get_post( $this->id ); - return rest_ensure_response( $post ); + return $post; } } 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 index b6814709cd472..dd71e9733283c 100644 --- 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 @@ -38,7 +38,7 @@ public function register_routes() { array( array( 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'install_font' ), + 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), 'args' => array ( 'slug' => array( @@ -63,19 +63,106 @@ 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' ), + ), + ) + ); + + 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' ), + ), + ) + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\/\w-]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'uninstall_font' ), + 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), ) ); } + /** + * Get item (font family). + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $post = get_post( $request['id'] ); + if ( ! $post ) { + return new WP_Error( + 'rest_invalid_id', + __( 'Invalid font family ID.' ), + 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). + * + * @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, + ); + $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 ) { + $item = json_decode( $post->post_content, true ); + if ( null == $item ) { + return $item; + } + $item['id'] = $post->ID; + return $item; + } + /** * Returns validation errors in font faces data for installation. * @@ -170,11 +257,16 @@ public function validate_font_faces( $param, $request ) { * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function uninstall_font( $request ) { + public function delete_item( $request ) { $font_family = new WP_Font_Family( $request['id'] ); $result = $font_family->uninstall(); + if ( is_wp_error( $result ) ) { - $result->add_data( array( 'status' => 500 ) ); + 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 ); } @@ -248,7 +340,7 @@ private function needs_write_permission( $font_family ) { * in the request parameters. * @return WP_REST_Response|WP_Error The updated Font Library post content. */ - public function install_font( $request ) { + public function create_item( $request ) { $font_family_data = array ( 'slug' => $request->get_param( 'slug' ), 'name' => $request->get_param( 'name' ), @@ -274,7 +366,7 @@ public function install_font( $request ) { $font_family = new WP_Font_Family(); $font_family->set_data( $font_family_data ); $result = $font_family->install( $files ); - - return rest_ensure_response( $result ); + $response = $this->prepare_item_for_response( $result, $request ); + return rest_ensure_response( $response ); } } From 1c23f0977ed320734b05c79e05d28741c513becf Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Wed, 4 Oct 2023 14:06:04 -0400 Subject: [PATCH 080/105] Refactored font-family GET and DELETE api endpoints to use SLUG instead of ID. --- .../fonts/class-wp-font-family.php | 80 ++++------- ...class-wp-rest-font-families-controller.php | 136 +++++++++--------- 2 files changed, 91 insertions(+), 125 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family.php b/src/wp-includes/fonts/class-wp-font-family.php index fc50484f6e0aa..aed278124e719 100644 --- a/src/wp-includes/fonts/class-wp-font-family.php +++ b/src/wp-includes/fonts/class-wp-font-family.php @@ -33,18 +33,10 @@ class WP_Font_Family { * @param array $font_data Font family data. * @throws Exception If the font family data is missing the slug. */ - public function __construct( $id = null ) { - $this->id = $id; - } - - /** - * Sets the font family data. - * - * @since 6.4.0 - * - * @param array $font_data An array in fontFamily theme.json format. - */ - public function set_data( $font_data ) { + public function __construct( $font_data ) { + if ( empty( $font_data['slug'] ) ) { + throw new Exception( 'Font family data is missing the slug.' ); + } $this->data = $font_data; } @@ -109,15 +101,21 @@ private function remove_font_family_assets() { * @return bool|WP_Error True if the font family was uninstalled, WP_Error otherwise. */ public function uninstall() { - $data = $this->populate_data_from_post_content_by_id(); - if ( is_wp_error( $data ) ) { - return $data; + $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( $this->id, true ) + ! wp_delete_post( $post->ID, true ) ) { return new WP_Error( 'font_family_not_deleted', @@ -455,33 +453,6 @@ private function download_or_move_font_faces( $files ) { return false; } - /** - * Populates the data for this object from the database using a post id. - * - * @since 6.4.0 - * - * @return bool|WP_Error True if the data was populated, WP_Error otherwise. - */ - private function populate_data_from_post_content_by_id() { - $post = get_post( $this->id ); - if ( $post && !empty( $post ) ) { - $post_content_data = json_decode( $post->post_content, true ); - // If the post content is not valid JSON, return null. - if ( is_null( $post_content_data ) ) { - return new WP_Error( - 'font_family_post_content_not_valid_json', - __( 'The font family post content is not valid JSON.' ) - ); - } - $this->set_data( $post_content_data ); - return true; - } - return new WP_Error( - 'font_family_not_found', - __( 'The font family could not be found.' ) - ); - } - /** * Gets a post for a font family by its slug. * @@ -489,14 +460,14 @@ private function populate_data_from_post_content_by_id() { * * @return WP_Post|null The post if the post exists, null otherwise. */ - private function get_post_by_slug() { + 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; @@ -511,7 +482,7 @@ private function get_post_by_slug() { * * @since 6.4.0 * - * @return bool|WP_Error True if the post was created, WP_Error otherwise. + * @return int|WP_Error Post ID if the post was created, WP_Error otherwise. */ private function create_font_post() { $post = array( @@ -529,8 +500,7 @@ private function create_font_post() { __( 'Font post creation failed.' ) ); } - $this->id = $post_id; - return true; + return $post_id; } /** @@ -563,7 +533,7 @@ private function get_intersecting_font_faces( $existing, $incoming ) { * @since 6.4.0 * * @param WP_Post $post The post to update. - * @return bool|WP_Error True if the update was successful, WP_Error otherwise. + * @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 ); @@ -572,7 +542,7 @@ private function update_font_post( $post ) { $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'] ); } @@ -605,7 +575,7 @@ private function update_font_post( $post ) { ); } - return true; + return $post_id; } /** @@ -622,13 +592,11 @@ private function create_or_update_font_post() { $post = $this->get_post_by_slug(); - if( $post ){ - $this->id = $post->ID; + if ( $post ) { return $this->update_font_post( $post ); } - + return $this->create_font_post(); - } /** @@ -657,7 +625,7 @@ public function install( $files = null ) { if ( is_wp_error( $result ) ) { return $result; } - $post = get_post( $this->id ); + $post = get_post( $result ); return $post; } } 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 index dd71e9733283c..e698aff1ca920 100644 --- 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 @@ -1,4 +1,5 @@ rest_base, array( array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_item' ), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), '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' ), - ), - ) ), ) ); register_rest_route( $this->namespace, - '/' . $this->rest_base, + '/' . $this->rest_base . '/(?P[\/\w-]+)', array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), + 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), ) @@ -77,19 +60,38 @@ public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\/\w-]+)', + '/' . $this->rest_base, array( array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), + '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' ), + ), + ), ), ) ); register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\/\w-]+)', + '/' . $this->rest_base . '/(?P[\/\w-]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, @@ -107,11 +109,12 @@ public function register_routes() { * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { - $post = get_post( $request['id'] ); + $font_family = new WP_Font_Family( array( 'slug' => $request['slug'] ) ); + $post = $font_family->get_post_by_slug(); if ( ! $post ) { return new WP_Error( - 'rest_invalid_id', - __( 'Invalid font family ID.' ), + 'font_family_slug_not_found', + __( 'Font Family with that slug was not found.' ), array( 'status' => 404, ) @@ -130,37 +133,32 @@ public function get_item( $request ) { return rest_ensure_response( $item ); } - /** - * Get items (font families). - * - * @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, - ); - $posts = get_posts( $args ); - $response = array(); - foreach ( $posts as $post ) { - $item = $this->prepare_item_for_response( $post, $request ); + /** + * Get items (font families). + * + * @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, + ); + $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 ); - } + } + } + return rest_ensure_response( $response ); + } public function prepare_item_for_response( $post, $request ) { - $item = json_decode( $post->post_content, true ); - if ( null == $item ) { - return $item; - } - $item['id'] = $post->ID; - return $item; + return json_decode( $post->post_content, true ); } /** @@ -229,7 +227,7 @@ private function get_validation_errors( $font_faces, $files ) { * @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 ); + $font_faces = json_decode( $param, true ); if ( null === $font_faces ) { return new WP_Error( 'rest_invalid_param', @@ -238,12 +236,12 @@ public function validate_font_faces( $param, $request ) { ); } - $files = $request->get_file_params(); + $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 $validation; } return true; @@ -258,9 +256,10 @@ public function validate_font_faces( $param, $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( $request['id'] ); - $result = $font_family->uninstall(); - + + $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 ) ); @@ -341,7 +340,7 @@ private function needs_write_permission( $font_family ) { * @return WP_REST_Response|WP_Error The updated Font Library post content. */ public function create_item( $request ) { - $font_family_data = array ( + $font_family_data = array( 'slug' => $request->get_param( 'slug' ), 'name' => $request->get_param( 'name' ), 'fontFamily' => $request->get_param( 'fontFamily' ), @@ -362,11 +361,10 @@ public function create_item( $request ) { } // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - $font_family = new WP_Font_Family(); - $font_family->set_data( $font_family_data ); - $result = $font_family->install( $files ); - $response = $this->prepare_item_for_response( $result, $request ); + $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 ); } } From 4f7fd91da5108b5604efaaca6deff7abdaac4d8a Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Wed, 4 Oct 2023 14:11:51 -0400 Subject: [PATCH 081/105] Added or refactored tests for Font Family createItem, deleteItem, getItem, getItems and registerRoutes --- .../base.php | 0 .../getFontCollection.php | 0 .../getFontCollections.php | 0 .../registerRoutes.php | 22 + .../wpRestFontFamiliesController/base.php | 43 ++ .../createItem.php | 308 +++++++++++++ .../deleteItem.php | 66 +++ .../wpRestFontFamiliesController/getItem.php | 96 ++++ .../wpRestFontFamiliesController/getItems.php | 112 +++++ .../registerRoutes.php | 25 + .../installFonts.php | 432 ------------------ .../registerRoutes.php | 27 -- .../uninstallFonts.php | 95 ---- 13 files changed, 672 insertions(+), 554 deletions(-) rename tests/phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontCollectionsController}/base.php (100%) rename tests/phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontCollectionsController}/getFontCollection.php (100%) rename tests/phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontCollectionsController}/getFontCollections.php (100%) create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/createItem.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/deleteItem.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItem.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/getItems.php create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/registerRoutes.php delete mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php delete mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php delete mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php similarity index 100% rename from tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php rename to tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php similarity index 100% rename from tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php rename to tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php similarity index 100% rename from tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php rename to tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php new file mode 100644 index 0000000000000..1390f62fb2cc2 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php @@ -0,0 +1,22 @@ +get_routes(); + $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'Rest server has not the collections path initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); + } +} 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/fonts/font-library/wpRestFontLibraryController/installFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php deleted file mode 100644 index 4edc5e08908fa..0000000000000 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php +++ /dev/null @@ -1,432 +0,0 @@ -set_param( 'font_families', $font_families_json ); - $install_request->set_file_params( $files ); - $response = rest_get_server()->dispatch( $install_request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertCount( count( $expected_response['successes'] ), $data['successes'], 'Not all the font families were installed correctly.' ); - - // Checks that the font families were installed correctly. - for ( $family_index = 0; $family_index < count( $data['successes'] ); $family_index++ ) { - $installed_font = $data['successes'][ $family_index ]; - $expected_font = $expected_response['successes'][ $family_index ]; - - if ( isset( $installed_font['fontFace'] ) || isset( $expected_font['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $installed_font['fontFace'] ); $face_index++ ) { - // Checks that the font asset were created correctly. - if ( isset( $installed_font['fontFace'][ $face_index ]['src'] ) ) { - $this->assertStringEndsWith( $expected_font['fontFace'][ $face_index ]['src'], $installed_font['fontFace'][ $face_index ]['src'], 'The src of the fonts were not updated as expected.' ); - } - // Removes the src from the response to compare the rest of the data. - unset( $installed_font['fontFace'][ $face_index ]['src'] ); - unset( $expected_font['fontFace'][ $face_index ]['src'] ); - unset( $installed_font['fontFace'][ $face_index ]['uploadedFile'] ); - } - } - - // Compares if the rest of the data is the same. - $this->assertEquals( $expected_font, $installed_font, 'The endpoint answer is not as expected.' ); - } - } - - /** - * Data provider for test_install_fonts - */ - public function data_install_fonts() { - - $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); - copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $temp_file_path1 ); - - $temp_file_path2 = wp_tempnam( 'Monteserrat-' ); - copy( path_join( DIR_TESTDATA, 'fonts/Merriweather.ttf' ), $temp_file_path2 ); - - return array( - - 'google_fonts_to_download' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), - ), - ), - ), - 'files' => array(), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', - ), - ), - ), - ), - 'errors' => array(), - ), - ), - - 'google_fonts_to_use_as_is' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), - ), - ), - ), - 'files' => array(), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - - ), - ), - ), - ), - 'errors' => array(), - ), - ), - - 'fonts_without_font_faces' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', - ), - ), - 'files' => array(), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', - ), - ), - 'errors' => array(), - ), - ), - - 'fonts_with_local_fonts_assets' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'uploadedFile' => 'files1', - ), - ), - ), - ), - 'files' => array( - 'files0' => array( - 'name' => 'piazzola1.ttf', - 'type' => 'font/ttf', - 'tmp_name' => $temp_file_path1, - 'error' => 0, - 'size' => 123, - ), - 'files1' => array( - 'name' => 'montserrat1.ttf', - 'type' => 'font/ttf', - 'tmp_name' => $temp_file_path2, - 'error' => 0, - 'size' => 123, - ), - ), - 'expected_response' => array( - 'successes' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', - ), - ), - ), - - ), - 'errors' => array(), - ), - ), - ); - } - - /** - * Tests failure when fonfaces has improper inputs - * - * @dataProvider data_install_with_improper_inputs - * - * @param array $font_families Font families to install in theme.json format. - * @param array $files Font files to install. - */ - public function test_install_with_improper_inputs( $font_families, $files = array() ) { - $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); - $font_families_json = json_encode( $font_families ); - $install_request->set_param( 'font_families', $font_families_json ); - $install_request->set_file_params( $files ); - - $response = rest_get_server()->dispatch( $install_request ); - $this->assertSame( 400, $response->get_status() ); - } - - /** - * Data provider for test_install_with_improper_inputs - */ - public function data_install_with_improper_inputs() { - $temp_file_path1 = wp_tempnam( 'Piazzola1-' ); - file_put_contents( $temp_file_path1, 'Mocking file content' ); - - return array( - 'not a font families array' => array( - 'font_families' => 'This is not an array', - ), - - 'empty array' => array( - 'font_families' => array(), - ), - - 'without slug' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - ), - ), - ), - - 'with improper font face property' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => 'This is not an array', - ), - ), - ), - - 'with empty font face property' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array(), - ), - ), - ), - - 'fontface referencing uploaded file without uploaded files' => array( - 'font_families' => array( - 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_families' => array( - 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_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'uploadedFile' => 'files0', - ), - ), - ), - ), - ), - ); - } -} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php deleted file mode 100644 index 8bc9efd0fc52f..0000000000000 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php +++ /dev/null @@ -1,27 +0,0 @@ -get_routes(); - $this->assertArrayHasKey( '/wp/v2/fonts', $routes, 'Rest server has not the fonts path intialized.' ); - $this->assertCount( 2, $routes['/wp/v2/fonts'], 'Rest server has not the 2 fonts paths initialized.' ); - $this->assertCount( 1, $routes['/wp/v2/fonts/collections'], 'Rest server has not the collections path initialized.' ); - $this->assertCount( 1, $routes['/wp/v2/fonts/collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); - - $this->assertArrayHasKey( 'POST', $routes['/wp/v2/fonts'][0]['methods'], 'Rest server has not the POST method for fonts intialized.' ); - $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/fonts'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); - } -} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php deleted file mode 100644 index a45e910b27473..0000000000000 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php +++ /dev/null @@ -1,95 +0,0 @@ - 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), - ), - ), - ); - - $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); - $font_families_json = json_encode( $mock_families ); - $install_request->set_param( 'font_families', $font_families_json ); - rest_get_server()->dispatch( $install_request ); - } - - public function test_uninstall() { - $font_families_to_uninstall = array( - array( - 'slug' => 'piazzolla', - ), - array( - 'slug' => 'montserrat', - ), - ); - - $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); - $uninstall_request->set_param( 'font_families', $font_families_to_uninstall ); - $response = rest_get_server()->dispatch( $uninstall_request ); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - } - - - public function test_uninstall_non_existing_fonts() { - $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); - - $non_existing_font_data = array( - array( - 'slug' => 'non-existing-font', - 'name' => 'Non existing font', - ), - array( - 'slug' => 'another-not-installed-font', - 'name' => 'Another not installed font', - ), - ); - - $uninstall_request->set_param( 'font_families', $non_existing_font_data ); - $response = rest_get_server()->dispatch( $uninstall_request ); - $data = $response->get_data(); - $this->assertCount( 2, $data['errors'], 'The response should have 2 errors, one for each font family uninstall failure.' ); - } -} From 14ca3bdd328fac1d0403922fa80f55063235dff8 Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Wed, 4 Oct 2023 14:28:33 -0400 Subject: [PATCH 082/105] Refactored Font Family tests to use getPostBySlug --- .../{getFontPost.php => getPostBySlug.php} | 10 +++++----- .../tests/fonts/font-library/wpFontFamily/install.php | 4 ++-- .../fonts/font-library/wpFontFamily/uninstall.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename tests/phpunit/tests/fonts/font-library/wpFontFamily/{getFontPost.php => getPostBySlug.php} (77%) diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getPostBySlug.php similarity index 77% rename from tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php rename to tests/phpunit/tests/fonts/font-library/wpFontFamily/getPostBySlug.php index 7cdb90ec47ca0..01153d275194c 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/getFontPost.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/getPostBySlug.php @@ -1,6 +1,6 @@ merriweather['font_data'] ); // Test. - $actual = $font->get_font_post(); + $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' ); } @@ -37,6 +37,6 @@ public function test_should_return_post() { public function test_should_return_null_when_post_does_not_exist() { $font = new WP_Font_Family( $this->merriweather['font_data'] ); - $this->assertNull( $font->get_font_post() ); + $this->assertNull( $font->get_post_by_slug() ); } } diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php index cc20557ee9c87..af096e34df3c6 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/install.php @@ -27,7 +27,7 @@ public function test_should_not_download_when_no_fontface( $font_data ) { // Test. $font->install(); $this->assertEmpty( $this->files_in_dir( static::$fonts_dir ), 'Font directory should be empty' ); - $this->assertInstanceOf( WP_Post::class, $font->get_font_post(), 'Font post should exist after install' ); + $this->assertInstanceOf( WP_Post::class, $font->get_post_by_slug(), 'Font post should exist after install' ); } /** @@ -97,7 +97,7 @@ public function test_should_download_fontfaces_and_create_post( $font_data, arra $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_font_post(), 'Font post should exist after install' ); + $this->assertInstanceOf( WP_Post::class, $font->get_post_by_slug(), 'Font post should exist after install' ); } /** diff --git a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php index f73ba7a6ffe9a..b0537e89bc33c 100644 --- a/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php +++ b/tests/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php @@ -78,14 +78,14 @@ public function test_should_uninstall( $font_data, array $files_data ) { $font->install( $files_data ); // Pre-checks to ensure the starting point is as expected. - $this->assertInstanceOf( WP_Post::class, $font->get_font_post(), 'Font post should exist' ); + $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_font_post(), 'Font post should be deleted after uninstall' ); + $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' ); } From 8d9f49654c2c9d653ebb080fd85ec2d3f1e20e95 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 17:16:09 -0300 Subject: [PATCH 083/105] use snake_case schema in font collections endpoints --- ...ss-wp-rest-font-collections-controller.php | 83 +++++++++++++++---- 1 file changed, 69 insertions(+), 14 deletions(-) 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 index 76590210ca42a..836d59ee785e0 100644 --- 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 @@ -41,7 +41,7 @@ public function register_routes() { 'callback' => array( $this, 'get_font_collections' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), - 'schema' => array( $this, 'get_font_collections_schema' ), + 'schema' => array( $this, 'get_items_schema' ), ) ); @@ -61,7 +61,7 @@ public function register_routes() { 'callback' => array( $this, 'get_font_collection' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), - 'schema' => array( $this, 'get_public_item_schema' ), + 'schema' => array( $this, 'get_item_schema' ), ) ); } @@ -89,7 +89,8 @@ public function get_font_collection( $request ) { return $collection_with_data; } - return rest_ensure_response( $collection_with_data ); + $response = $this->prepare_item_for_response( $collection_with_data, $request ); + return rest_ensure_response( $response ); } /** @@ -102,7 +103,7 @@ public function get_font_collection( $request ) { public function get_font_collections() { $collections = array(); foreach ( WP_Font_Library::get_font_collections() as $collection ) { - $collections[] = $collection->get_config(); + $collections[] = $this->prepare_item_for_response( $collection->get_config(), null ); } return new WP_REST_Response( $collections, 200 ); @@ -135,8 +136,12 @@ public function update_font_library_permissions_check() { * * @return array Item schema data. */ - public function get_font_collections_schema() { - return array( + 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' => 'object', @@ -164,6 +169,9 @@ public function get_font_collections_schema() { ), ), ); + + $this->schema = $schema; + return $this->add_additional_fields_schema( $this->schema ); } /** @@ -204,7 +212,7 @@ public function get_item_schema() { 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'properties' => array( - 'fontFamilies' => array( + 'font_families' => array( 'description' => __( 'List of font families.' ), 'type' => 'array', 'items' => array( @@ -214,7 +222,7 @@ public function get_item_schema() { 'description' => __( 'Name of the font family.' ), 'type' => 'string', ), - 'fontFamily' => array( + 'font_family' => array( 'description' => __( 'Font family string.' ), 'type' => 'string', ), @@ -226,25 +234,25 @@ public function get_item_schema() { 'description' => __( 'Category of the font family.' ), 'type' => 'string', ), - 'fontFace' => array( + 'font_face' => array( 'description' => __( 'Font face details.' ), 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( - 'downloadFromUrl' => array( + 'download_from_url' => array( 'description' => __( 'URL to download the font.' ), 'type' => 'string', ), - 'fontWeight' => array( + 'font_weight' => array( 'description' => __( 'Font weight.' ), 'type' => 'string', ), - 'fontStyle' => array( + 'fonts_style' => array( 'description' => __( 'Font style.' ), 'type' => 'string', ), - 'fontFamily' => array( + 'font_family' => array( 'description' => __( 'Font family string.' ), 'type' => 'string', ), @@ -268,8 +276,55 @@ public function get_item_schema() { ); $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 ); + } + } From 1243b318b23fc658117ed81434259b50f22ff46b Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 17:18:07 -0300 Subject: [PATCH 084/105] update api endpoint routes test and fixture --- .../tests/rest-api/rest-schema-setup.php | 11 +- tests/qunit/fixtures/wp-api-generated.js | 1191 ++++------------- 2 files changed, 249 insertions(+), 953 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 2e7b24338cf14..feaae40d70363 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -133,9 +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/fonts', - '/wp/v2/fonts/collections', - '/wp/v2/fonts/collections/(?P[\/\w-]+)', + '/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', @@ -189,10 +190,6 @@ public function test_expected_routes_in_schema() { '/wp-site-health/v1/tests/authorization-header', '/wp-site-health/v1/tests/page-cache', '/wp-site-health/v1/directory-sizes', - '/wp/v2/wp_font_family', - '/wp/v2/wp_font_family/(?P[\d]+)', - '/wp/v2/wp_font_family/(?P[\d]+)/autosaves', - '/wp/v2/wp_font_family/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/wp_pattern_category', '/wp/v2/wp_pattern_category/(?P[\d]+)', ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 8e8b77ed5803a..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": "", @@ -7416,7 +7410,192 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/wp_font_family": { + "/wp/v2/types": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ], + "_links": { + "self": "http://example.org/index.php?rest_route=/wp/v2/types" + } + }, + "/wp/v2/types/(?P[\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "type": { + "description": "An alphanumeric identifier for the post type.", + "type": "string", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, + "/wp/v2/statuses": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ], + "_links": { + "self": "http://example.org/index.php?rest_route=/wp/v2/statuses" + } + }, + "/wp/v2/statuses/(?P[\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "status": { + "description": "An alphanumeric identifier for the status.", + "type": "string", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, + "/wp/v2/taxonomies": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "type": { + "description": "Limit results to taxonomies associated with a specific post type.", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": "http://example.org/index.php?rest_route=/wp/v2/taxonomies" + } + }, + "/wp/v2/taxonomies/(?P[\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "taxonomy": { + "description": "An alphanumeric identifier for the taxonomy.", + "type": "string", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, + "/wp/v2/categories": { "namespace": "wp/v2", "methods": [ "GET", @@ -7462,30 +7641,6 @@ mockedApiResponse.Schema = { "type": "string", "required": false }, - "after": { - "description": "Limit response to posts published after a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "modified_after": { - "description": "Limit response to posts modified after a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "before": { - "description": "Limit response to posts published before a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "modified_before": { - "description": "Limit response to posts modified before a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, "exclude": { "description": "Ensure result set excludes specific IDs.", "type": "array", @@ -7504,15 +7659,10 @@ mockedApiResponse.Schema = { "default": [], "required": false }, - "offset": { - "description": "Offset the result set by a specific number of items.", - "type": "integer", - "required": false - }, "order": { "description": "Order sort attribute ascending or descending.", "type": "string", - "default": "desc", + "default": "asc", "enum": [ "asc", "desc" @@ -7520,860 +7670,25 @@ mockedApiResponse.Schema = { "required": false }, "orderby": { - "description": "Sort collection by post attribute.", + "description": "Sort collection by term attribute.", "type": "string", - "default": "date", + "default": "name", "enum": [ - "author", - "date", "id", "include", - "modified", - "parent", - "relevance", + "name", "slug", "include_slugs", - "title" + "term_group", + "description", + "count" ], "required": false }, - "search_columns": { - "default": [], - "description": "Array of column names to be searched.", - "type": "array", - "items": { - "enum": [ - "post_title", - "post_content", - "post_excerpt" - ], - "type": "string" - }, - "required": false - }, - "slug": { - "description": "Limit result set to posts with one or more specific slugs.", - "type": "array", - "items": { - "type": "string" - }, - "required": false - }, - "status": { - "default": "publish", - "description": "Limit result set to posts assigned one or more statuses.", - "type": "array", - "items": { - "enum": [ - "publish", - "future", - "draft", - "pending", - "private", - "trash", - "auto-draft", - "inherit", - "request-pending", - "request-confirmed", - "request-failed", - "request-completed", - "any" - ], - "type": "string" - }, - "required": false - } - } - }, - { - "methods": [ - "POST" - ], - "allow_batch": { - "v1": true - }, - "args": { - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "content": { - "description": "The content for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Content for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML content for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - } - } - } - ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" - } - ] - } - }, - "/wp/v2/wp_font_family/(?P[\\d]+)": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "allow_batch": { - "v1": true - }, - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "password": { - "description": "The password for the post if it is password protected.", - "type": "string", - "required": false - } - } - }, - { - "methods": [ - "POST", - "PUT", - "PATCH" - ], - "allow_batch": { - "v1": true - }, - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "content": { - "description": "The content for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Content for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML content for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - } - } - }, - { - "methods": [ - "DELETE" - ], - "allow_batch": { - "v1": true - }, - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "force": { - "type": "boolean", - "default": false, - "description": "Whether to bypass Trash and force deletion.", - "required": false - } - } - } - ] - }, - "/wp/v2/wp_font_family/(?P[\\d]+)/autosaves": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - }, - { - "methods": [ - "POST" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "content": { - "description": "The content for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Content for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML content for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - } - } - } - ] - }, - "/wp/v2/wp_font_family/(?P[\\d]+)/autosaves/(?P[\\d]+)": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "id": { - "description": "The ID for the autosave.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ] - }, - "/wp/v2/types": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ], - "_links": { - "self": "http://example.org/index.php?rest_route=/wp/v2/types" - } - }, - "/wp/v2/types/(?P[\\w-]+)": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "type": { - "description": "An alphanumeric identifier for the post type.", - "type": "string", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ] - }, - "/wp/v2/statuses": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ], - "_links": { - "self": "http://example.org/index.php?rest_route=/wp/v2/statuses" - } - }, - "/wp/v2/statuses/(?P[\\w-]+)": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "status": { - "description": "An alphanumeric identifier for the status.", - "type": "string", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ] - }, - "/wp/v2/taxonomies": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "type": { - "description": "Limit results to taxonomies associated with a specific post type.", - "type": "string", - "required": false - } - } - } - ], - "_links": { - "self": "http://example.org/index.php?rest_route=/wp/v2/taxonomies" - } - }, - "/wp/v2/taxonomies/(?P[\\w-]+)": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "taxonomy": { - "description": "An alphanumeric identifier for the taxonomy.", - "type": "string", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - } - ] - }, - "/wp/v2/categories": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "allow_batch": { - "v1": true - }, - "args": { - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "page": { - "description": "Current page of the collection.", - "type": "integer", - "default": 1, - "minimum": 1, - "required": false - }, - "per_page": { - "description": "Maximum number of items to be returned in result set.", - "type": "integer", - "default": 10, - "minimum": 1, - "maximum": 100, - "required": false - }, - "search": { - "description": "Limit results to those matching a string.", - "type": "string", - "required": false - }, - "exclude": { - "description": "Ensure result set excludes specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "include": { - "description": "Limit result set to specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "order": { - "description": "Order sort attribute ascending or descending.", - "type": "string", - "default": "asc", - "enum": [ - "asc", - "desc" - ], - "required": false - }, - "orderby": { - "description": "Sort collection by term attribute.", - "type": "string", - "default": "name", - "enum": [ - "id", - "include", - "name", - "slug", - "include_slugs", - "term_group", - "description", - "count" - ], - "required": false - }, - "hide_empty": { - "description": "Whether to hide terms not assigned to any posts.", - "type": "boolean", - "default": false, + "hide_empty": { + "description": "Whether to hide terms not assigned to any posts.", + "type": "boolean", + "default": false, "required": false }, "parent": { @@ -9464,8 +8779,7 @@ mockedApiResponse.Schema = { "wp_block": "wp_block", "wp_template": "wp_template", "wp_template_part": "wp_template_part", - "wp_navigation": "wp_navigation", - "wp_font_family": "wp_font_family" + "wp_navigation": "wp_navigation" } }, "required": false @@ -12150,45 +11464,39 @@ mockedApiResponse.Schema = { ] } }, - "/wp/v2/fonts": { + "/wp/v2/font-families": { "namespace": "wp/v2", "methods": [ - "POST", - "DELETE" + "GET", + "POST" ], "endpoints": [ { "methods": [ - "POST" + "GET" ], - "args": { - "font_families": { - "type": "string", - "required": true - } - } + "args": [] }, { "methods": [ - "DELETE" + "POST" ], "args": { - "font_families": { - "type": "array", - "description": "The font families to uninstall.", - "minItems": 1, - "items": { - "required": true, - "type": "object", - "properties": { - "slug": { - "type": "string", - "description": "The font family slug.", - "required": true - } - } - }, + "slug": { + "type": "string", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "fontFamily": { + "type": "string", "required": true + }, + "fontFace": { + "type": "string", + "required": false } } } @@ -12196,12 +11504,33 @@ mockedApiResponse.Schema = { "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/fonts" + "href": "http://example.org/index.php?rest_route=/wp/v2/font-families" } ] } }, - "/wp/v2/fonts/collections": { + "/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" @@ -12217,12 +11546,12 @@ mockedApiResponse.Schema = { "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/fonts/collections" + "href": "http://example.org/index.php?rest_route=/wp/v2/font-collections" } ] } }, - "/wp/v2/fonts/collections/(?P[\\/\\w-]+)": { + "/wp/v2/font-collections/(?P[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ "GET" @@ -12234,7 +11563,7 @@ mockedApiResponse.Schema = { ], "args": { "id": { - "description": "The id of a font collection.", + "description": "Unique identifier for the post.", "type": "string", "required": true } @@ -13307,36 +12636,6 @@ mockedApiResponse.TypesCollection = { } ] } - }, - "wp_font_family": { - "description": "Font Family definition for installed fonts.", - "hierarchical": false, - "has_archive": false, - "name": "Font Family", - "slug": "wp_font_family", - "icon": null, - "taxonomies": [], - "rest_base": "wp_font_family", - "rest_namespace": "wp/v2", - "_links": { - "collection": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/types" - } - ], - "wp:items": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/wp_font_family" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } } }; From c5c6ec47ffaf3af526fa1bf9d453033461606014 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 17:35:42 -0300 Subject: [PATCH 085/105] fix the font collections schema to accomodate arrays instead of a single object --- ...ss-wp-rest-font-collections-controller.php | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) 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 index 836d59ee785e0..a353d6b854f67 100644 --- 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 @@ -144,28 +144,31 @@ public function get_items_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'font-collections', - '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' => __( 'Link to the list of font families.' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), + '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' ), + ), ), ), ); From f398761a1ff2dab1380dac9524fbd043a48b5d76 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 00:23:59 +0200 Subject: [PATCH 086/105] 1. Remove the registerRoutes test. 2. Create a new test class - Tests_Fonts_WpRestFontCollectionsController. --- .../wpRestFontCollectionsController.php | 99 +++++++++++++++++++ .../registerRoutes.php | 22 ----- 2 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php delete mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php 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..8915bc19aec3d --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -0,0 +1,99 @@ +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.' ); + } + + public function set_up() { + parent::set_up(); + + static::$fonts_dir = WP_Font_Library::get_fonts_dir(); + + // Create a user with administrator role. + $admin_id = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $admin_id ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + parent::tear_down(); + + // Reset $collections static property of WP_Font_Library class. + $reflection = new ReflectionClass( 'WP_Font_Library' ); + $property = $reflection->getProperty( 'collections' ); + $property->setAccessible( true ); + $property->setValue( null, array() ); + + // Clean up the /fonts directory. + foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { + @unlink( $file ); + } + } + + public function test_context_param() { + + } + + public function test_get_items() { + + } + + public function test_get_item() { + + } + + public function test_create_item() { + + } + + public function test_update_item() { + + } + + function test_delete_item() { + + } + + public function test_prepare_item() { + + } + + public function test_get_item_schema() { + + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php deleted file mode 100644 index 1390f62fb2cc2..0000000000000 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php +++ /dev/null @@ -1,22 +0,0 @@ -get_routes(); - $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'Rest server has not the collections path initialized.' ); - $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); - } -} From d85e545c346743113df8807c8c439c278fcc4e70 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 00:29:00 +0200 Subject: [PATCH 087/105] Fix CS. --- .../font-library/wpRestFontCollectionsController.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index 8915bc19aec3d..e9194b33ac11d 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -66,34 +66,26 @@ public function tear_down() { } public function test_context_param() { - } public function test_get_items() { - } public function test_get_item() { - } public function test_create_item() { - } public function test_update_item() { - } function test_delete_item() { - } public function test_prepare_item() { - } public function test_get_item_schema() { - } } From 07d01ff91e33c943a22184561265e3e95d7a188f Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 00:30:55 +0200 Subject: [PATCH 088/105] Fix CS. --- .../fonts/font-library/wpRestFontCollectionsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index e9194b33ac11d..d281e9789dcf7 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -80,7 +80,7 @@ public function test_create_item() { public function test_update_item() { } - function test_delete_item() { + public function test_delete_item() { } public function test_prepare_item() { From ec5211338660d9203accfe3d579c1fc685c8833f Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 4 Oct 2023 23:55:04 -0300 Subject: [PATCH 089/105] grouping font-families endpoints and add response schemas. --- ...class-wp-rest-font-families-controller.php | 182 ++++++++++++++++-- 1 file changed, 163 insertions(+), 19 deletions(-) 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 index e698aff1ca920..d031fcd6cb973 100644 --- 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 @@ -43,25 +43,6 @@ public function register_routes() { 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), - ) - ); - - 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' ), - ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), @@ -86,6 +67,7 @@ public function register_routes() { ), ), ), + 'schema' => array( $this, 'get_items_schema' ), ) ); @@ -93,11 +75,17 @@ public function register_routes() { $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' ), ) ); } @@ -367,4 +355,160 @@ public function create_item( $request ) { $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', + ), + ), + ), + ); + } } From 39677265bd7feb4dfd015aa894a259396c415db3 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 5 Oct 2023 00:05:55 -0300 Subject: [PATCH 090/105] add default order for font families --- .../endpoints/class-wp-rest-font-families-controller.php | 2 ++ 1 file changed, 2 insertions(+) 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 index d031fcd6cb973..29ba8bca607fc 100644 --- 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 @@ -133,6 +133,8 @@ public function get_items( $request ) { '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(); From a19ac36ebc3027352736dfd198469321f6d1f786 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 5 Oct 2023 08:57:15 -0300 Subject: [PATCH 091/105] add since 6.4 comment Co-authored-by: Mukesh Panchal --- .../endpoints/class-wp-rest-font-families-controller.php | 2 ++ 1 file changed, 2 insertions(+) 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 index 29ba8bca607fc..417a67081cd03 100644 --- 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 @@ -93,6 +93,8 @@ public function register_routes() { /** * Get item (font family). * + * @since 6.4.0 + * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ From 05d3ff77775a3bebb28ac93fc6177fe2e718cad6 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 5 Oct 2023 08:57:39 -0300 Subject: [PATCH 092/105] Remove extra new line Co-authored-by: Mukesh Panchal --- .../endpoints/class-wp-rest-font-collections-controller.php | 1 - 1 file changed, 1 deletion(-) 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 index a353d6b854f67..6ce9451b6fd6f 100644 --- 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 @@ -281,7 +281,6 @@ public function get_item_schema() { $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } - /** * Convert string from camelCase to snake_case. * From 963d226ac011cab8d70f187afaf8a56491cfa541 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 5 Oct 2023 08:58:41 -0300 Subject: [PATCH 093/105] remove extra spaces Co-authored-by: Mukesh Panchal --- src/wp-includes/fonts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/fonts.php b/src/wp-includes/fonts.php index a32195efef4e2..bfff198b0150a 100644 --- a/src/wp-includes/fonts.php +++ b/src/wp-includes/fonts.php @@ -60,7 +60,7 @@ function wp_print_font_faces( $fonts = array() ) { * @param string[] $config { * Font collection associative array of configuration options. * - * @type string $id The font collection's unique ID. + * @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 From e28134f56db2bd9e0d03f8539d3694bd1eba1f95 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 5 Oct 2023 09:28:32 -0300 Subject: [PATCH 094/105] add missing 'since 6.4' comment --- .../endpoints/class-wp-rest-font-families-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 417a67081cd03..0adb3ad33d73f 100644 --- 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 @@ -16,8 +16,6 @@ * @since 6.4.0 */ class WP_REST_Font_Families_Controller extends WP_REST_Controller { - - /** * Constructor. * @@ -125,7 +123,9 @@ public function get_item( $request ) { /** * Get items (font families). - * + * + * @since 6.4.0 + * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ From b2a96fc43bc7934f7a4df07a6c38c5d6888f668d Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 14:54:12 +0200 Subject: [PATCH 095/105] Get rid of WP_REST_Font_Library_Controller_UnitTestCase. --- .../wpRestFontCollectionsController/base.php | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php deleted file mode 100644 index cebd4af33ebbe..0000000000000 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php +++ /dev/null @@ -1,49 +0,0 @@ -factory->user->create( - array( - 'role' => 'administrator', - ) - ); - wp_set_current_user( $admin_id ); - } - - /** - * Tear down each test method. - */ - public function tear_down() { - parent::tear_down(); - - // Reset $collections static property of WP_Font_Library class. - $reflection = new ReflectionClass( 'WP_Font_Library' ); - $property = $reflection->getProperty( 'collections' ); - $property->setAccessible( true ); - $property->setValue( null, array() ); - - // Clean up the /fonts directory. - foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { - @unlink( $file ); - } - } -} From 640159e41c306c13e8872db7501dc643f8d6673d Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 14:58:28 +0200 Subject: [PATCH 096/105] Fix parent class name. --- .../wpRestFontCollectionsController.php | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index d281e9789dcf7..605bd82f1c556 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -7,7 +7,7 @@ * * @group restapi */ -class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_Controller_Testcase { +class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_TestCase { /** * Fonts directory (in uploads). @@ -16,23 +16,6 @@ class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_Controlle */ protected static $fonts_dir; - /** - * @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.' ); - } - public function set_up() { parent::set_up(); @@ -65,6 +48,23 @@ public function tear_down() { } } + /** + * @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.' ); + } + public function test_context_param() { } From d44da254d8c01ec6cfcdec89e127bcdbc18de564 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 5 Oct 2023 10:15:22 -0300 Subject: [PATCH 097/105] fix typo in comment --- src/wp-includes/fonts/class-wp-font-family-utils.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-family-utils.php b/src/wp-includes/fonts/class-wp-font-family-utils.php index 538c35fb6120e..b5a246a66d300 100644 --- a/src/wp-includes/fonts/class-wp-font-family-utils.php +++ b/src/wp-includes/fonts/class-wp-font-family-utils.php @@ -1,8 +1,8 @@ Date: Thu, 5 Oct 2023 15:29:15 +0200 Subject: [PATCH 098/105] Rename controller methods. --- .../class-wp-rest-font-collections-controller.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index 6ce9451b6fd6f..b42b098e21523 100644 --- 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 @@ -38,7 +38,7 @@ public function register_routes() { array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collections' ), + 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), 'schema' => array( $this, 'get_items_schema' ), @@ -58,7 +58,7 @@ public function register_routes() { ), array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collection' ), + 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), 'schema' => array( $this, 'get_item_schema' ), @@ -74,7 +74,7 @@ public function register_routes() { * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function get_font_collection( $request ) { + public function get_item( $request ) { $id = $request['id']; $collection = WP_Font_Library::get_font_collection( $id ); // If the collection doesn't exist returns a 404. @@ -89,7 +89,7 @@ public function get_font_collection( $request ) { return $collection_with_data; } - $response = $this->prepare_item_for_response( $collection_with_data, $request ); + $response = $this->prepare_item_for_response( $collection_with_data, $request ); return rest_ensure_response( $response ); } @@ -100,7 +100,7 @@ public function get_font_collection( $request ) { * * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function get_font_collections() { + public function get_items() { $collections = array(); foreach ( WP_Font_Library::get_font_collections() as $collection ) { $collections[] = $this->prepare_item_for_response( $collection->get_config(), null ); @@ -293,7 +293,7 @@ private function camel_to_snake( $input ) { $output = _wp_to_kebab_case( $input ); return str_replace( '-', '_', $output ); } - + /** * Convert array keys from camelCase to snake_case. * From 7382406aac290fb0cf135f9497b04e3702709785 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 18:45:04 +0200 Subject: [PATCH 099/105] Fix 404 errors. --- .../wpRestFontCollectionsController.php | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index 605bd82f1c556..ecaae2db52ecb 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -64,17 +64,46 @@ public function test_register_routes() { $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.' ); } - - public function test_context_param() { + 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() ); } 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.' ); + } + + + + public function test_context_param() { } public function test_get_item() { } public function test_create_item() { + } public function test_update_item() { From d9ffea937df62d2b81b403dc473f80cd0d15d892 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 20:13:48 +0200 Subject: [PATCH 100/105] Move the tests to Tests_Fonts_WpRestFontCollectionsController. --- .../wpRestFontCollectionsController.php | 119 ++++++++++++++++-- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index ecaae2db52ecb..16099809fe737 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -28,13 +28,57 @@ public function set_up() { ) ); wp_set_current_user( $admin_id ); + + + // 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_request' ), 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 ); } /** * Tear down each test method. */ public function tear_down() { - parent::tear_down(); + + // Remove the mock to not affect other tests. + remove_filter( 'pre_http_request', array( $this, 'mock_request' ) ); // Reset $collections static property of WP_Font_Library class. $reflection = new ReflectionClass( 'WP_Font_Library' ); @@ -46,6 +90,30 @@ public function tear_down() { foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { @unlink( $file ); } + + parent::tear_down(); + } + + 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; } /** @@ -64,13 +132,6 @@ public function test_register_routes() { $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.' ); } - 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() ); - } - public function test_get_items() { // Mock font collection data file. $mock_file = wp_tempnam( 'my-collection-data-' ); @@ -94,6 +155,48 @@ public function test_get_items() { $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' ); } + public function test_get_font_collection_from_file() { + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/one-collection' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); + $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' ); + $this->assertSame( array ( 'this_is_mock_data' => true, ), $data['data'], 'The response data does not have the expected file data.' ); + } + + public function test_get_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() ); + } + + public function test_get_font_collection_from_url() { + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-url' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); + $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' ); + } + + public function test_get_non_existing_collection_should_return_404() { + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/non-existing-collection-id' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + public function test_get_non_existing_file_should_return_500() { + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-non-existing-file' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 500, $response->get_status() ); + } + + public function test_get_non_existing_url_should_return_500() { + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-non-existing-url' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 500, $response->get_status() ); + } + public function test_context_param() { From 9edfb39ec99b5ddab291b36df04b0778c0ee6cb7 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 20:30:08 +0200 Subject: [PATCH 101/105] Fix the tests. --- .../font-library/wpRestFontCollectionsController.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index 16099809fe737..b5ed090e219c9 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -14,7 +14,12 @@ class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_TestCase * * @var string */ - protected static $fonts_dir; + private static $fonts_dir; + + private static $skip_setup_test_methods = array( + 'test_get_items_with_no_collection_registered', + 'test_get_items' + ); public function set_up() { parent::set_up(); @@ -28,6 +33,9 @@ public function set_up() { ) ); wp_set_current_user( $admin_id ); + if ( in_array( $this->getName(), self::$skip_setup_test_methods)) { + return; + } // Mock font collection data file. From c4de43f6e9d0ed1cb86a9218e925f54b1eb72d51 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 23:52:00 +0200 Subject: [PATCH 102/105] Improve PHPDOC blocks. --- .../wpRestFontCollectionsController.php | 143 +++++++++++++----- 1 file changed, 101 insertions(+), 42 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index b5ed090e219c9..614504cfe9351 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -16,9 +16,9 @@ class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_TestCase */ private static $fonts_dir; - private static $skip_setup_test_methods = array( + private static $test_methods_to_skip_setup = array( 'test_get_items_with_no_collection_registered', - 'test_get_items' + 'test_get_items', ); public function set_up() { @@ -33,16 +33,16 @@ public function set_up() { ) ); wp_set_current_user( $admin_id ); - if ( in_array( $this->getName(), self::$skip_setup_test_methods)) { + + 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_request' ), 10, 3 ); + add_filter( 'pre_http_request', array( $this, 'mock_response' ), 10, 3 ); $config_with_file = array( 'id' => 'one-collection', @@ -80,13 +80,10 @@ public function set_up() { wp_register_font_collection( $config_with_non_existing_url ); } - /** - * Tear down each test method. - */ public function tear_down() { // Remove the mock to not affect other tests. - remove_filter( 'pre_http_request', array( $this, 'mock_request' ) ); + remove_filter( 'pre_http_request', array( $this, 'mock_response' ) ); // Reset $collections static property of WP_Font_Library class. $reflection = new ReflectionClass( 'WP_Font_Library' ); @@ -102,26 +99,35 @@ public function tear_down() { parent::tear_down(); } - public function mock_request( $preempt, $args, $url ) { + /** + * @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 ) { - - // Mock the response body. - $mock_collection_data = array( - 'fontFamilies' => 'mock', - 'categories' => 'mock', - ); - - return array( - 'body' => json_encode( $mock_collection_data ), - 'response' => array( - 'code' => 200, - ), - ); + 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; } - // 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, + ), + ); } /** @@ -140,6 +146,11 @@ public function test_register_routes() { $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-' ); @@ -163,15 +174,10 @@ public function test_get_items() { $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' ); } - public function test_get_font_collection_from_file() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/one-collection' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); - $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' ); - $this->assertSame( array ( 'this_is_mock_data' => true, ), $data['data'], 'The response data does not have the expected file data.' ); - } - + /** + * @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 ); @@ -179,7 +185,11 @@ public function test_get_items_with_no_collection_registered() { $this->assertSame( array(), $response->get_data() ); } - public function test_get_font_collection_from_url() { + /** + * @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(); @@ -187,40 +197,89 @@ public function test_get_font_collection_from_url() { $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() { } - public function test_get_item() { - } - + /** + * @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 + ) + ); } public function test_prepare_item() { From 9deeb4a4f8930ffb4e4f5f71ffac34aa0cdf38bb Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Thu, 5 Oct 2023 23:53:08 +0200 Subject: [PATCH 103/105] Fix @return type. --- .../endpoints/class-wp-rest-font-collections-controller.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index b42b098e21523..641547137445e 100644 --- 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 @@ -98,9 +98,11 @@ public function get_item( $request ) { * * @since 6.4.0 * - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + * @param WP_REST_Request $request Full details about the request. * + * @return WP_REST_Response Response object. + * */ - public function get_items() { + 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 ); From 6c9e8569cceeb682c132e5b04c88781f5759cb95 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Mon, 9 Oct 2023 19:15:11 +0200 Subject: [PATCH 104/105] Implement the test for Tests_Fonts_WpRestFontCollectionsController::test_prepare_item. --- .../wpRestFontCollectionsController.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index 614504cfe9351..15b172b858d4c 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -7,7 +7,7 @@ * * @group restapi */ -class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_TestCase { +class Tests_Fonts_WpRestFontCollectionsController extends WP_Test_REST_Controller_Testcase { /** * Fonts directory (in uploads). @@ -282,7 +282,28 @@ public function test_delete_item() { ); } + /** + * @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']['font_families'], $response_font_collection_data['data']['font_families'], 'Font families should be consistent in the response.' ); } public function test_get_item_schema() { From 89cd0009075fd02dc0549ba13152ae1718d25672 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Mon, 9 Oct 2023 19:39:29 +0200 Subject: [PATCH 105/105] Fix the test. --- .../fonts/font-library/wpRestFontCollectionsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index 15b172b858d4c..8df997d7c16fb 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -303,7 +303,7 @@ public function test_prepare_item() { $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']['font_families'], $response_font_collection_data['data']['font_families'], 'Font families should be consistent in the response.' ); + $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() {