diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php new file mode 100644 index 00000000000000..019cf0f3f0b29d --- /dev/null +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php @@ -0,0 +1,263 @@ +slug = sanitize_title( $slug ); + + if ( is_array( $data_or_file ) ) { + $this->data = $this->sanitize_and_validate_data( $data_or_file ); + } else { + // JSON data is lazy loaded by ::get_data(). + $this->src = $data_or_file; + } + + if ( $this->slug !== $slug ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Font collection slug. */ + sprintf( __( 'Font collection slug "%s" is not valid. Slugs must use only alphanumeric characters, dashes, and underscores.', 'gutenberg' ), $slug ), + '6.5.0' + ); + } + } + + /** + * Retrieves the font collection data. + * + * @since 6.5.0 + * + * @return array|WP_Error An array containing the font collection data, or a WP_Error on failure. + */ + public function get_data() { + // If the collection uses JSON data, load it and cache the data/error. + if ( $this->src && empty( $this->data ) ) { + $this->data = $this->load_from_json( $this->src ); + } + + if ( is_wp_error( $this->data ) ) { + return $this->data; + } + + // Set defaults for optional properties. + $defaults = array( + 'description' => '', + 'categories' => array(), + ); + return wp_parse_args( $this->data, $defaults ); + } + + /** + * Loads font collection data from a JSON file or URL. + * + * @since 6.5.0 + * + * @param string $file_or_url File path or URL to a JSON file containing the font collection data. + * @return array|WP_Error An array containing the font collection data on success, + * else an instance of WP_Error on failure. + */ + private function load_from_json( $file_or_url ) { + $url = wp_http_validate_url( $file_or_url ); + $file = file_exists( $file_or_url ) ? wp_normalize_path( realpath( $file_or_url ) ) : false; + + if ( ! $url && ! $file ) { + // translators: %s: File path or URL to font collection JSON file. + $message = __( 'Font collection JSON file is invalid or does not exist.', 'gutenberg' ); + _doing_it_wrong( __METHOD__, $message, '6.5.0' ); + return new WP_Error( 'font_collection_json_missing', $message ); + } + + return $url ? $this->load_from_url( $url ) : $this->load_from_file( $file ); + } + + /** + * Loads the font collection data from a JSON file path. + * + * @since 6.5.0 + * + * @param string $file File path to a JSON file containing the font collection data. + * @return array|WP_Error An array containing the font collection data on success, + * else an instance of WP_Error on failure. + */ + private function load_from_file( $file ) { + $data = wp_json_file_decode( $file, array( 'associative' => true ) ); + if ( empty( $data ) ) { + return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.', 'gutenberg' ) ); + } + + return $this->sanitize_and_validate_data( $data ); + } + + /** + * Loads the font collection data from a JSON file URL. + * + * @since 6.5.0 + * + * @param string $url URL to a JSON file containing the font collection data. + * @return array|WP_Error An array containing the font collection data on success, + * else an instance of WP_Error on failure. + */ + private function load_from_url( $url ) { + // Limit key to 167 characters to avoid failure in the case of a long URL. + $transient_key = substr( 'wp_font_collection_url_' . $url, 0, 167 ); + $data = get_site_transient( $transient_key ); + + if ( false === $data ) { + $response = wp_safe_remote_get( $url ); + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + // translators: %s: Font collection URL. + return new WP_Error( 'font_collection_request_error', sprintf( __( 'Error fetching the font collection data from "%s".', 'gutenberg' ), $url ) ); + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( empty( $data ) ) { + return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the REST response JSON.', 'gutenberg' ) ); + } + + // Make sure the data is valid before storing it in a transient. + $data = $this->sanitize_and_validate_data( $data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + set_site_transient( $transient_key, $data, DAY_IN_SECONDS ); + } + + return $data; + } + + /** + * Sanitizes and validates the font collection data. + * + * @since 6.5.0 + * + * @param array $data Font collection data to sanitize and validate. + * @return array|WP_Error Sanitized data if valid, otherwise a WP_Error instance. + */ + private function sanitize_and_validate_data( $data ) { + $schema = self::get_sanitization_schema(); + $data = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + $required_properties = array( 'name', 'font_families' ); + foreach ( $required_properties as $property ) { + if ( empty( $data[ $property ] ) ) { + $message = sprintf( + // translators: 1: Font collection slug, 2: Missing property name. + __( 'Font collection "%1$s" has missing or empty property: "%2$s."', 'gutenberg' ), + $this->slug, + $property + ); + _doing_it_wrong( __METHOD__, $message, '6.5.0' ); + return new WP_Error( 'font_collection_missing_property', $message ); + } + } + + return $data; + } + + /** + * Retrieves the font collection sanitization schema. + * + * @since 6.5.0 + * + * @return array Font collection sanitization schema. + */ + private static function get_sanitization_schema() { + return array( + 'name' => 'sanitize_text_field', + 'description' => 'sanitize_text_field', + 'font_families' => array( + array( + 'font_family_settings' => array( + 'name' => 'sanitize_text_field', + 'slug' => 'sanitize_title', + 'fontFamily' => 'sanitize_text_field', + 'preview' => 'sanitize_url', + 'fontFace' => array( + array( + 'fontFamily' => 'sanitize_text_field', + 'fontStyle' => 'sanitize_text_field', + 'fontWeight' => 'sanitize_text_field', + 'src' => function ( $value ) { + return is_array( $value ) + ? array_map( 'sanitize_text_field', $value ) + : sanitize_text_field( $value ); + }, + 'preview' => 'sanitize_url', + 'fontDisplay' => 'sanitize_text_field', + 'fontStretch' => 'sanitize_text_field', + 'ascentOverride' => 'sanitize_text_field', + 'descentOverride' => 'sanitize_text_field', + 'fontVariant' => 'sanitize_text_field', + 'fontFeatureSettings' => 'sanitize_text_field', + 'fontVariationSettings' => 'sanitize_text_field', + 'lineGapOverride' => 'sanitize_text_field', + 'sizeAdjust' => 'sanitize_text_field', + 'unicodeRange' => 'sanitize_text_field', + ), + ), + ), + 'categories' => array( 'sanitize_title' ), + ), + ), + 'categories' => array( + array( + 'name' => 'sanitize_text_field', + 'slug' => 'sanitize_title', + ), + ), + ); + } + } +} diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php new file mode 100644 index 00000000000000..9bf90ca1d9dd8a --- /dev/null +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php @@ -0,0 +1,148 @@ +is_collection_registered( $new_collection->slug ) ) { + $error_message = sprintf( + /* translators: %s: Font collection slug. */ + __( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ), + $new_collection->slug + ); + _doing_it_wrong( + __METHOD__, + $error_message, + '6.5.0' + ); + return new WP_Error( 'font_collection_registration_error', $error_message ); + } + $this->collections[ $new_collection->slug ] = $new_collection; + return $new_collection; + } + + /** + * Unregisters a previously registered font collection. + * + * @since 6.5.0 + * + * @param string $slug Font collection slug. + * @return bool True if the font collection was unregistered successfully and false otherwise. + */ + public function unregister_font_collection( $slug ) { + if ( ! $this->is_collection_registered( $slug ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Font collection slug. */ + sprintf( __( 'Font collection "%s" not found.' ), $slug ), + '6.5.0' + ); + return false; + } + unset( $this->collections[ $slug ] ); + return true; + } + + /** + * Checks if a font collection is registered. + * + * @since 6.5.0 + * + * @param string $slug Font collection slug. + * @return bool True if the font collection is registered and false otherwise. + */ + private function is_collection_registered( $slug ) { + return array_key_exists( $slug, $this->collections ); + } + + /** + * Gets all the font collections available. + * + * @since 6.5.0 + * + * @return array List of font collections. + */ + public function get_font_collections() { + return $this->collections; + } + + /** + * Gets a font collection. + * + * @since 6.5.0 + * + * @param string $slug Font collection slug. + * @return WP_Font_Collection|WP_Error Font collection object, + * or WP_Error object if the font collection doesn't exist. + */ + public function get_font_collection( $slug ) { + if ( array_key_exists( $slug, $this->collections ) ) { + return $this->collections[ $slug ]; + } + return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 6.5.0 + * + * @return WP_Font_Library The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + } +} diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php new file mode 100644 index 00000000000000..b75ce0ec41cf69 --- /dev/null +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php @@ -0,0 +1,237 @@ + '', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'fontStretch' => '100%', + 'unicodeRange' => 'U+0-10FFFF', + ); + $settings = wp_parse_args( $settings, $defaults ); + + $font_family = mb_strtolower( $settings['fontFamily'] ); + $font_style = strtolower( $settings['fontStyle'] ); + $font_weight = strtolower( $settings['fontWeight'] ); + $font_stretch = strtolower( $settings['fontStretch'] ); + $unicode_range = strtoupper( $settings['unicodeRange'] ); + + // Convert weight keywords to numeric strings. + $font_weight = str_replace( array( 'normal', 'bold' ), array( '400', '700' ), $font_weight ); + + // Convert stretch keywords to numeric strings. + $font_stretch_map = array( + 'ultra-condensed' => '50%', + 'extra-condensed' => '62.5%', + 'condensed' => '75%', + 'semi-condensed' => '87.5%', + 'normal' => '100%', + 'semi-expanded' => '112.5%', + 'expanded' => '125%', + 'extra-expanded' => '150%', + 'ultra-expanded' => '200%', + ); + $font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch ); + + $slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range ); + + $slug_elements = array_map( + function ( $elem ) { + // Remove quotes to normalize font-family names, and ';' to use as a separator. + $elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) ); + + // Normalize comma separated lists by removing whitespace in between items, + // but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts). + // CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE, + // which by default are all matched by \s in PHP. + return preg_replace( '/,\s+/', ',', $elem ); + }, + $slug_elements + ); + + return sanitize_text_field( join( ';', $slug_elements ) ); + } + + /** + * Sanitizes a tree of data using a schema. + * + * The schema structure should mirror the data tree. Each value provided in the + * schema should be a callable that will be applied to sanitize the corresponding + * value in the data tree. Keys that are in the data tree, but not present in the + * schema, will be removed in the santized data. Nested arrays are traversed recursively. + * + * @since 6.5.0 + * + * @access private + * + * @param array $tree The data to sanitize. + * @param array $schema The schema used for sanitization. + * @return array The sanitized data. + */ + public static function sanitize_from_schema( $tree, $schema ) { + if ( ! is_array( $tree ) || ! is_array( $schema ) ) { + return array(); + } + + foreach ( $tree as $key => $value ) { + // Remove keys not in the schema or with null/empty values. + if ( ! array_key_exists( $key, $schema ) ) { + unset( $tree[ $key ] ); + continue; + } + + $is_value_array = is_array( $value ); + $is_schema_array = is_array( $schema[ $key ] ) && ! is_callable( $schema[ $key ] ); + + if ( $is_value_array && $is_schema_array ) { + if ( wp_is_numeric_array( $value ) ) { + // If indexed, process each item in the array. + foreach ( $value as $item_key => $item_value ) { + $tree[ $key ][ $item_key ] = isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) + ? self::sanitize_from_schema( $item_value, $schema[ $key ][0] ) + : self::apply_sanitizer( $item_value, $schema[ $key ][0] ); + } + } else { + // If it is an associative or indexed array, process as a single object. + $tree[ $key ] = self::sanitize_from_schema( $value, $schema[ $key ] ); + } + } elseif ( ! $is_value_array && $is_schema_array ) { + // If the value is not an array but the schema is, remove the key. + unset( $tree[ $key ] ); + } elseif ( ! $is_schema_array ) { + // If the schema is not an array, apply the sanitizer to the value. + $tree[ $key ] = self::apply_sanitizer( $value, $schema[ $key ] ); + } + + // Remove keys with null/empty values. + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } + + return $tree; + } + + /** + * Applies a sanitizer function to a value. + * + * @since 6.5.0 + * + * @param mixed $value The value to sanitize. + * @param mixed $sanitizer The sanitizer function to apply. + * @return mixed The sanitized value. + */ + private static function apply_sanitizer( $value, $sanitizer ) { + if ( null === $sanitizer ) { + return $value; + + } + return call_user_func( $sanitizer, $value ); + } + + /** + * 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.5.0 + * + * @return Array A collection of mime types keyed by file extension. + */ + public static function get_allowed_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', + ); + } + } +} diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php similarity index 84% rename from lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php rename to lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php index 3711015bb275ad..06a073d426abc1 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php @@ -74,7 +74,7 @@ public function register_routes() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { - $collections_all = WP_Font_Library::get_font_collections(); + $collections_all = WP_Font_Library::get_instance()->get_font_collections(); $page = $request['page']; $per_page = $request['per_page']; @@ -84,7 +84,7 @@ public function get_items( $request ) { if ( $page > $max_pages && $total_items > 0 ) { return new WP_Error( 'rest_post_invalid_page_number', - __( 'The page number requested is larger than the number of pages available.', 'default' ), + __( 'The page number requested is larger than the number of pages available.' ), array( 'status' => 400 ) ); } @@ -94,8 +94,10 @@ public function get_items( $request ) { $items = array(); foreach ( $collections_page as $collection ) { $item = $this->prepare_item_for_response( $collection, $request ); + + // If there's an error loading a collection, skip it and continue loading valid collections. if ( is_wp_error( $item ) ) { - return $item; + continue; } $item = $this->prepare_response_for_collection( $item ); $items[] = $item; @@ -140,7 +142,7 @@ public function get_items( $request ) { */ public function get_item( $request ) { $slug = $request->get_param( 'slug' ); - $collection = WP_Font_Library::get_font_collection( $slug ); + $collection = WP_Font_Library::get_instance()->get_font_collection( $slug ); // If the collection doesn't exist returns a 404. if ( is_wp_error( $collection ) ) { @@ -148,48 +150,38 @@ public function get_item( $request ) { return $collection; } - $item = $this->prepare_item_for_response( $collection, $request ); - - if ( is_wp_error( $item ) ) { - return $item; - } - - return $item; + return $this->prepare_item_for_response( $collection, $request ); } - /* + /** * Prepare a single collection output for response. * * @since 6.5.0 * * @param WP_Font_Collection $collection Collection object. * @param WP_REST_Request $request Request object. - * @return array|WP_Error + * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $collection, $request ) { $fields = $this->get_fields_for_response( $request ); $item = array(); - $config_fields = array( 'slug', 'name', 'description' ); - foreach ( $config_fields as $field ) { - if ( in_array( $field, $fields, true ) ) { - $item[ $field ] = $collection->$field; - } + if ( rest_is_field_included( 'slug', $fields ) ) { + $item['slug'] = $collection->slug; } - $data_fields = array( 'font_families', 'categories' ); - if ( in_array( 'font_families', $fields, true ) || in_array( 'categories', $fields, true ) ) { - $content = $collection->get_content(); - - // If there was an error getting the collection data, return the error. - if ( is_wp_error( $content ) ) { - $content->add_data( array( 'status' => 500 ) ); - return $content; + // If any data fields are requested, get the collection data. + $data_fields = array( 'name', 'description', 'font_families', 'categories' ); + if ( ! empty( array_intersect( $fields, $data_fields ) ) ) { + $collection_data = $collection->get_data(); + if ( is_wp_error( $collection_data ) ) { + $collection_data->add_data( array( 'status' => 500 ) ); + return $collection_data; } foreach ( $data_fields as $field ) { - if ( in_array( $field, $fields, true ) ) { - $item[ $field ] = $content[ $field ]; + if ( rest_is_field_included( $field, $fields ) ) { + $item[ $field ] = $collection_data[ $field ]; } } } @@ -212,9 +204,9 @@ public function prepare_item_for_response( $collection, $request ) { * * @since 6.5.0 * - * @param WP_REST_Response $response The response object. + * @param WP_REST_Response $response The response object. * @param WP_Font_Collection $collection The Font Collection object. - * @param WP_REST_Request $request Request used to generate the response. + * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'rest_prepare_font_collection', $response, $collection, $request ); } @@ -279,7 +271,7 @@ public function get_item_schema() { * @return array Links for the given font collection. */ protected function prepare_links( $collection ) { - $links = array( + return array( 'self' => array( 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $collection->slug ) ), ), @@ -287,7 +279,6 @@ protected function prepare_links( $collection ) { 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ); - return $links; } /** @@ -322,17 +313,17 @@ public function get_collection_params() { * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. */ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_cannot_read', - __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); + if ( current_user_can( 'edit_theme_options' ) ) { + return true; } - return true; + + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); } } } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php similarity index 85% rename from lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php rename to lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php index 01ffce1eea4838..60f04e9ff23df3 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php @@ -151,7 +151,7 @@ public function get_item_permissions_check( $request ) { * * @param string $value Encoded JSON string of font face settings. * @param WP_REST_Request $request Request object. - * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. */ public function validate_create_font_face_settings( $value, $request ) { $settings = json_decode( $value, true ); @@ -187,20 +187,32 @@ public function validate_create_font_face_settings( $value, $request ) { } } - $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + $files = $request->get_file_params(); - // Check that srcs are non-empty strings. - $filtered_src = array_filter( array_filter( $srcs, 'is_string' ) ); - if ( empty( $filtered_src ) ) { - return new WP_Error( - 'rest_invalid_param', - __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ), - array( 'status' => 400 ) - ); + foreach ( $srcs as $src ) { + // Check that each src is a non-empty string. + $src = ltrim( $src ); + if ( empty( $src ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Check that srcs are valid URLs or file references. + if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: src value in the font face settings. */ + sprintf( __( 'font_face_settings[src] value "%s" must be a valid URL or file reference.', 'gutenberg' ), $src ), + array( 'status' => 400 ) + ); + } } // Check that each file in the request references a src in the settings. - $files = $request->get_file_params(); foreach ( array_keys( $files ) as $file ) { if ( ! in_array( $file, $srcs, true ) ) { return new WP_Error( @@ -227,9 +239,12 @@ public function validate_create_font_face_settings( $value, $request ) { public function sanitize_font_face_settings( $value ) { // Settings arrive as stringified JSON, since this is a multipart/form-data request. $settings = json_decode( $value, true ); + $schema = $this->get_item_schema()['properties']['font_face_settings']['properties']; - if ( isset( $settings['fontFamily'] ) ) { - $settings['fontFamily'] = WP_Font_Utils::format_font_family( $settings['fontFamily'] ); + // Sanitize settings based on callbacks in the schema. + foreach ( $settings as $key => $value ) { + $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; + $settings[ $key ] = call_user_func( $sanitize_callback, $value ); } return $settings; @@ -509,11 +524,17 @@ public function get_item_schema() { 'description' => __( 'CSS font-family value.', 'gutenberg' ), 'type' => 'string', 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), + ), ), 'fontStyle' => array( 'description' => __( 'CSS font-style value.', 'gutenberg' ), 'type' => 'string', 'default' => 'normal', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontWeight' => array( 'description' => __( 'List of available font weights, separated by a space.', 'gutenberg' ), @@ -521,6 +542,9 @@ public function get_item_schema() { // Changed from `oneOf` to avoid errors from loose type checking. // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check. 'type' => array( 'string', 'integer' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontDisplay' => array( 'description' => __( 'CSS font-display value.', 'gutenberg' ), @@ -533,10 +557,14 @@ public function get_item_schema() { 'swap', 'optional', ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'src' => array( 'description' => __( 'Paths or URLs to the font files.', 'gutenberg' ), - // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array. + // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array, + // and causing a "matches more than one of the expected formats" error. 'anyOf' => array( array( 'type' => 'string', @@ -549,46 +577,83 @@ public function get_item_schema() { ), ), 'default' => array(), + 'arg_options' => array( + 'sanitize_callback' => function ( $value ) { + return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value ); + }, + ), ), 'fontStretch' => array( 'description' => __( 'CSS font-stretch value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'ascentOverride' => array( 'description' => __( 'CSS ascent-override value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'descentOverride' => array( 'description' => __( 'CSS descent-override value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontVariant' => array( 'description' => __( 'CSS font-variant value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontFeatureSettings' => array( 'description' => __( 'CSS font-feature-settings value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontVariationSettings' => array( 'description' => __( 'CSS font-variation-settings value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'lineGapOverride' => array( 'description' => __( 'CSS line-gap-override value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'sizeAdjust' => array( 'description' => __( 'CSS size-adjust value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'unicodeRange' => array( 'description' => __( 'CSS unicode-range value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'preview' => array( 'description' => __( 'URL to a preview image of the font face.', 'gutenberg' ), 'type' => 'string', + 'format' => 'uri', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_url', + ), ), ), 'required' => array( 'fontFamily', 'src' ), @@ -602,6 +667,26 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @since 6.5.0 + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + + $schema = parent::get_public_item_schema(); + + // Also remove `arg_options' from child font_family_settings properties, since the parent + // controller only handles the top level properties. + foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) { + unset( $property['arg_options'] ); + } + + return $schema; + } + /** * Retrieves the query params for the font face collection. * @@ -698,7 +783,7 @@ protected function get_parent_font_family_post( $font_family_id ) { */ protected function prepare_links( $post ) { // Entity meta. - $links = array( + return array( 'self' => array( 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), ), @@ -709,8 +794,6 @@ protected function prepare_links( $post ) { 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ), ), ); - - return $links; } /** @@ -741,6 +824,20 @@ protected function prepare_item_for_database( $request ) { return $prepared_post; } + /** + * Sanitizes a single src value for a font face. + * + * @since 6.5.0 + * + * @param string $value Font face src that is a URL or the key for a $_FILES array item. + * + * @return string Sanitized value. + */ + protected function sanitize_src( $value ) { + $value = ltrim( $value ); + return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value ); + } + /** * Handles the upload of a font file using wp_handle_upload(). * @@ -750,7 +847,7 @@ protected function prepare_item_for_database( $request ) { * @return array Array containing uploaded file attributes on success, or error on failure. */ protected function handle_font_file_upload( $file ) { - add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); add_filter( 'upload_dir', 'wp_get_font_dir' ); $overrides = array( @@ -764,13 +861,13 @@ protected function handle_font_file_upload( $file ) { // See wp_check_filetype_and_ext(). 'test_type' => true, // Only allow uploading font files for this request. - 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), + 'mimes' => WP_Font_Utils::get_allowed_font_mime_types(), ); $uploaded_file = wp_handle_upload( $file, $overrides ); remove_filter( 'upload_dir', 'wp_get_font_dir' ); - remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); return $uploaded_file; } @@ -788,7 +885,7 @@ public function handle_font_file_upload_error( $file, $message ) { $status = 500; $code = 'rest_font_upload_unknown_error'; - if ( __( 'Sorry, you are not allowed to upload this file type.', 'default' ) === $message ) { + if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) { $status = 400; $code = 'rest_font_upload_invalid_file_type'; } @@ -799,7 +896,7 @@ public function handle_font_file_upload_error( $file, $message ) { /** * Returns relative path to an uploaded font file. * - * The path is relative to the current fonts dir. + * The path is relative to the current fonts directory. * * @since 6.5.0 * @access private diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php similarity index 87% rename from lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php rename to lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php index 1f91d9e2457302..e4a2b2f8e97813 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php @@ -77,7 +77,7 @@ public function get_item_permissions_check( $request ) { * * @param string $value Encoded JSON string of font family settings. * @param WP_REST_Request $request Request object. - * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. */ public function validate_font_family_settings( $value, $request ) { $settings = json_decode( $value, true ); @@ -134,22 +134,21 @@ public function validate_font_family_settings( $value, $request ) { /** * Sanitizes the font family settings when creating or updating a font family. * - * @since 6.5.0 - * - * @param string $value Encoded JSON string of font family settings. - * @param WP_REST_Request $request Request object. - * @return array Decoded array font family settings. - */ + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font family settings. + * @param WP_REST_Request $request Request object. + * @return array Decoded array font family settings. + */ public function sanitize_font_family_settings( $value ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. $settings = json_decode( $value, true ); + $schema = $this->get_item_schema()['properties']['font_family_settings']['properties']; - if ( isset( $settings['fontFamily'] ) ) { - $settings['fontFamily'] = WP_Font_Utils::format_font_family( $settings['fontFamily'] ); - } - - // Provide default for preview, if not provided. - if ( ! isset( $settings['preview'] ) ) { - $settings['preview'] = ''; + // Sanitize settings based on callbacks in the schema. + foreach ( $settings as $key => $value ) { + $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; + $settings[ $key ] = call_user_func( $sanitize_callback, $value ); } return $settings; @@ -264,7 +263,7 @@ public function prepare_item_for_response( $item, $request ) { return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request ); } - /** + /** * Retrieves the post's schema, conforming to JSON Schema. * * @since 6.5.0 @@ -307,25 +306,39 @@ public function get_item_schema() { // Font family settings come directly from theme.json schema // See https://schemas.wp.org/trunk/theme.json 'font_family_settings' => array( - 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ), + 'description' => __( 'font-face definition in theme.json format.', 'gutenberg' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'properties' => array( 'name' => array( - 'description' => 'Name of the font family preset, translatable.', + 'description' => __( 'Name of the font family preset, translatable.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'slug' => array( - 'description' => 'Kebab-case unique identifier for the font family preset.', + 'description' => __( 'Kebab-case unique identifier for the font family preset.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), ), 'fontFamily' => array( - 'description' => 'CSS font-family value.', + 'description' => __( 'CSS font-family value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), + ), ), 'preview' => array( - 'description' => 'URL to a preview image of the font family.', + 'description' => __( 'URL to a preview image of the font family.', 'gutenberg' ), 'type' => 'string', + 'format' => 'uri', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_url', + ), ), ), 'required' => array( 'name', 'slug', 'fontFamily' ), @@ -339,6 +352,26 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @since 6.5.0 + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + + $schema = parent::get_public_item_schema(); + + // Also remove `arg_options' from child font_family_settings properties, since the parent + // controller only handles the top level properties. + foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) { + unset( $property['arg_options'] ); + } + + return $schema; + } + /** * Retrieves the query params for the font family collection. * @@ -405,7 +438,6 @@ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CRE * * @param int $font_family_id Font family post ID. * @return int[] Array of child font face post IDs. - * . */ protected function get_font_face_ids( $font_family_id ) { $query = new WP_Query( diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/compat/wordpress-6.5/fonts/fonts.php similarity index 78% rename from lib/experimental/fonts/font-library/font-library.php rename to lib/compat/wordpress-6.5/fonts/fonts.php index 5af21f11ebbb26..e6913b0d8a57eb 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/compat/wordpress-6.5/fonts/fonts.php @@ -19,7 +19,7 @@ * * @since 6.5.0 */ -function gutenberg_init_font_library_routes() { +function gutenberg_create_initial_post_types() { // @core-merge: This code will go into Core's `create_initial_post_types()`. $args = array( 'labels' => array( @@ -82,13 +82,30 @@ function gutenberg_init_font_library_routes() { 'autosave_rest_controller_class' => 'stdClass', ) ); +} +/** + * Initializes REST routes. + * + * @since 6.5 + */ +function gutenberg_create_initial_rest_routes() { // @core-merge: This code will go into Core's `create_initial_rest_routes()`. $font_collections_controller = new WP_REST_Font_Collections_Controller(); $font_collections_controller->register_routes(); } -add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); +/** + * Initializes REST routes and post types. + * + * @since 6.5 + */ +function gutenberg_init_font_library() { + gutenberg_create_initial_post_types(); + gutenberg_create_initial_rest_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_init_font_library' ); if ( ! function_exists( 'wp_register_font_collection' ) ) { @@ -97,18 +114,24 @@ function gutenberg_init_font_library_routes() { * * @since 6.5.0 * - * @param string[] $config { - * Font collection associative array of configuration options. + * @param string $slug Font collection slug. May only contain alphanumeric characters, dashes, + * and underscores. See sanitize_title(). + * @param array|string $data_or_file { + * Font collection data array or a path/URL to a JSON file containing the font collection. * - * @type string $id The font collection's unique ID. - * @type string $src The font collection's data as a JSON file path. - * @type array $data The font collection's data as a PHP array. + * @link https://schemas.wp.org/trunk/font-collection.json + * + * @type string $name Required. Name of the font collection shown in the Font Library. + * @type string $description Optional. A short descriptive summary of the font collection. Default empty. + * @type array $font_families Required. Array of font family definitions that are in the collection. + * @type array $categories Optional. Array of categories, each with a name and slug, that are used by the + * fonts in the collection. Default empty. * } - * @return WP_Font_Collection|WP_Error A font collection is it was registered - * successfully, else WP_Error. + * @return WP_Font_Collection|WP_Error A font collection if it was registered + * successfully, or WP_Error object on failure. */ - function wp_register_font_collection( $config ) { - return WP_Font_Library::register_font_collection( $config ); + function wp_register_font_collection( $slug, $data_or_file ) { + return WP_Font_Library::get_instance()->register_font_collection( $slug, $data_or_file ); } } @@ -118,22 +141,19 @@ function wp_register_font_collection( $config ) { * * @since 6.5.0 * - * @param string $collection_id The font collection ID. + * @param string $slug Font collection slug. + * @return bool True if the font collection was unregistered successfully, else false. */ - function wp_unregister_font_collection( $collection_id ) { - WP_Font_Library::unregister_font_collection( $collection_id ); + function wp_unregister_font_collection( $slug ) { + return WP_Font_Library::get_instance()->unregister_font_collection( $slug ); } - } -$google_fonts = array( - 'slug' => 'google-fonts', - 'name' => 'Google Fonts', - 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), - 'src' => 'https://s.w.org/images/fonts/17.6/collections/google-fonts-with-preview.json', -); - -wp_register_font_collection( $google_fonts ); +function gutenberg_register_font_collections() { + // TODO: update to production font collection URL. + wp_register_font_collection( 'google-fonts', 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/01aa57731575bd13f9db8d86ab80a2d74e28a1ac/releases/gutenberg-17.6/collections/google-fonts-with-preview.json' ); +} +add_action( 'init', 'gutenberg_register_font_collections' ); // @core-merge: This code should probably go into Core's src/wp-includes/functions.php. if ( ! function_exists( 'wp_get_font_dir' ) ) { @@ -152,7 +172,6 @@ function wp_unregister_font_collection( $collection_id ) { * @type string $baseurl URL path without subdir. * @type string|false $error False or error message. * } - * * @return array $defaults { * Array of information about the upload directory. * @@ -179,7 +198,15 @@ function wp_get_font_dir( $defaults = array() ) { $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; $defaults['error'] = false; - // Filters the fonts directory data. + /** + * Filters the fonts directory data. + * + * This filter allows developers to modify the fonts directory data. + * + * @since 6.5.0 + * + * @param array $defaults The original fonts directory data. + */ return apply_filters( 'font_dir', $defaults ); } } @@ -195,7 +222,6 @@ function wp_get_font_dir( $defaults = array() ) { * * @param int $post_id Post ID. * @param WP_Post $post Post object. - * @return void */ function _wp_after_delete_font_family( $post_id, $post ) { if ( 'wp_font_family' !== $post->post_type ) { @@ -225,7 +251,6 @@ function _wp_after_delete_font_family( $post_id, $post ) { * * @param int $post_id Post ID. * @param WP_Post $post Post object. - * @return void */ function _wp_before_delete_font_face( $post_id, $post ) { if ( 'wp_font_face' !== $post->post_type ) { @@ -275,7 +300,7 @@ function gutenberg_convert_legacy_font_family_format() { continue; } - $font_faces = $font_family_json['fontFace'] ?? array(); + $font_faces = isset( $font_family_json['fontFace'] ) ? $font_family_json['fontFace'] : array(); unset( $font_family_json['fontFace'] ); // Save wp_font_face posts within the family. @@ -290,7 +315,7 @@ function gutenberg_convert_legacy_font_family_format() { $font_face_id = wp_insert_post( wp_slash( $args ) ); - $file_urls = (array) $font_face['src'] ?? array(); + $file_urls = (array) ( isset( $font_face['src'] ) ? $font_face['src'] : array() ); foreach ( $file_urls as $file_url ) { // continue if the file is not local. @@ -306,7 +331,7 @@ function gutenberg_convert_legacy_font_family_format() { // Update the font family post to remove the font face data. $args = array(); $args['ID'] = $font_family->ID; - $args['post_title'] = $font_family_json['name'] ?? ''; + $args['post_title'] = isset( $font_family_json['name'] ) ? $font_family_json['name'] : ''; $args['post_name'] = sanitize_title( $font_family_json['slug'] ); unset( $font_family_json['name'] ); diff --git a/lib/experimental/fonts/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php b/lib/experimental/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php rename to lib/experimental/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider-local.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-provider-local.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider-local.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-provider-local.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-provider.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-provider.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-resolver.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-resolver.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-resolver.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-resolver.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-utils.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-utils.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-utils.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-utils.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts.php b/lib/experimental/font-face/bc-layer/class-wp-fonts.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-web-fonts.php b/lib/experimental/font-face/bc-layer/class-wp-web-fonts.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-web-fonts.php rename to lib/experimental/font-face/bc-layer/class-wp-web-fonts.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider-local.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts-provider-local.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider-local.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts-provider-local.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts-provider.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts-provider.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-utils.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts-utils.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-utils.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts-utils.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts.php diff --git a/lib/experimental/fonts/font-face/bc-layer/webfonts-deprecations.php b/lib/experimental/font-face/bc-layer/webfonts-deprecations.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/webfonts-deprecations.php rename to lib/experimental/font-face/bc-layer/webfonts-deprecations.php diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php deleted file mode 100644 index 22514151d0aa49..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-font-collection.php +++ /dev/null @@ -1,231 +0,0 @@ -is_config_valid( $config ); - - $this->slug = isset( $config['slug'] ) ? $config['slug'] : ''; - $this->name = isset( $config['name'] ) ? $config['name'] : ''; - $this->description = isset( $config['description'] ) ? $config['description'] : ''; - $this->src = isset( $config['src'] ) ? $config['src'] : ''; - $this->font_families = isset( $config['font_families'] ) ? $config['font_families'] : array(); - $this->categories = isset( $config['categories'] ) ? $config['categories'] : array(); - } - - /** - * Checks if the font collection config is valid. - * - * @since 6.5.0 - * - * @param array $config Font collection config options. { - * @type string $slug The font collection's unique slug. - * @type string $name The font collection's name. - * @type string $description The font collection's description. - * @type string $src The font collection's source. - * @type array $font_families An array of font families in the font collection. - * @type array $categories The font collection's categories. - * } - * @return bool True if the font collection config is valid and false otherwise. - */ - public static function is_config_valid( $config ) { - if ( empty( $config ) || ! is_array( $config ) ) { - _doing_it_wrong( __METHOD__, __( 'Font Collection config options are required as a non-empty array.', 'gutenberg' ), '6.5.0' ); - return false; - } - - $required_keys = array( 'slug', 'name' ); - foreach ( $required_keys as $key ) { - if ( empty( $config[ $key ] ) ) { - _doing_it_wrong( - __METHOD__, - // translators: %s: Font collection config key. - sprintf( __( 'Font Collection config %s is required as a non-empty string.', 'gutenberg' ), $key ), - '6.5.0' - ); - return false; - } - } - - if ( - ( empty( $config['src'] ) && empty( $config['font_families'] ) ) || - ( ! empty( $config['src'] ) && ! empty( $config['font_families'] ) ) - ) { - _doing_it_wrong( - __METHOD__, - sprintf( - /* translators: %1$s: src, %2$s: font_families */ - __( 'Font Collection config "%1$s" option OR "%2$s" option is required.', 'gutenberg' ), - 'src', - 'font_families' - ), - '6.5.0' - ); - return false; - } - - return true; - } - - /** - * Gets the font collection content. - * - * Load the font collection data from the src if it is not already loaded. - * - * @since 6.5.0 - * - * @return array|WP_Error { - * An array of font collection contents. - * - * @type array $font_families The font collection's font families. - * @type string $categories The font collection's categories. - * } - * - * A WP_Error object if there was an error loading the font collection data. - */ - public function get_content() { - // If the font families are not loaded, and the src is not empty, load the data from the src. - if ( empty( $this->font_families ) && ! empty( $this->src ) ) { - $data = $this->load_contents_from_src(); - if ( is_wp_error( $data ) ) { - return $data; - } - } - - return array( - 'font_families' => $this->font_families, - 'categories' => $this->categories, - ); - } - - /** - * Loads the font collection data from the src. - * - * @since 6.5.0 - * - * @return array|WP_Error An array containing the list of font families in font-collection.json format on success, - * else an instance of WP_Error on failure. - */ - private function load_contents_from_src() { - // If the src is a URL, fetch the data from the URL. - if ( preg_match( '#^https?://#', $this->src ) ) { - if ( ! wp_http_validate_url( $this->src ) ) { - return new WP_Error( 'font_collection_read_error', __( 'Invalid URL for Font Collection data.', 'gutenberg' ) ); - } - - $response = wp_remote_get( $this->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->src ) ) { - return new WP_Error( 'font_collection_read_error', __( 'Font Collection data JSON file does not exist.', 'gutenberg' ) ); - } - $data = wp_json_file_decode( $this->src, array( 'associative' => true ) ); - if ( empty( $data ) ) { - return new WP_Error( 'font_collection_read_error', __( 'Error reading the Font Collection data JSON file contents.', 'gutenberg' ) ); - } - } - - if ( empty( $data['font_families'] ) ) { - return new WP_Error( 'font_collection_contents_error', __( 'Font Collection data JSON file does not contain font families.', 'gutenberg' ) ); - } - - $this->font_families = $data['font_families']; - $this->categories = $data['categories'] ?? array(); - - return $data; - } - } -} diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php deleted file mode 100644 index 21234f47c4bb23..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ /dev/null @@ -1,161 +0,0 @@ -= 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', - ); - } - - /** - * Font collections. - * - * @since 6.5.0 - * - * @var array - */ - private static $collections = array(); - - /** - * Register a new font collection. - * - * @since 6.5.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 ) { - if ( ! WP_Font_Collection::is_config_valid( $config ) ) { - $error_message = __( 'Font collection config is invalid.', 'gutenberg' ); - return new WP_Error( 'font_collection_registration_error', $error_message ); - } - - $new_collection = new WP_Font_Collection( $config ); - - if ( self::is_collection_registered( $new_collection->slug ) ) { - $error_message = sprintf( - /* translators: %s: Font collection slug. */ - __( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ), - $config['slug'] - ); - _doing_it_wrong( - __METHOD__, - $error_message, - '6.5.0' - ); - return new WP_Error( 'font_collection_registration_error', $error_message ); - } - self::$collections[ $new_collection->slug ] = $new_collection; - return $new_collection; - } - - /** - * Unregisters a previously registered font collection. - * - * @since 6.5.0 - * - * @param string $collection_slug Font collection slug. - * @return bool True if the font collection was unregistered successfully and false otherwise. - */ - public static function unregister_font_collection( $slug ) { - if ( ! self::is_collection_registered( $slug ) ) { - _doing_it_wrong( - __METHOD__, - /* translators: %s: Font collection slug. */ - sprintf( __( 'Font collection "%s" not found.', 'default' ), $slug ), - '6.5.0' - ); - return false; - } - unset( self::$collections[ $slug ] ); - return true; - } - - /** - * Checks if a font collection is registered. - * - * @since 6.5.0 - * - * @param string $slug Font collection slug. - * @return bool True if the font collection is registered and false otherwise. - */ - private static function is_collection_registered( $slug ) { - return array_key_exists( $slug, self::$collections ); - } - - /** - * Gets all the font collections available. - * - * @since 6.5.0 - * - * @return array List of font collections. - */ - public static function get_font_collections() { - return self::$collections; - } - - /** - * Gets a font collection. - * - * @since 6.5.0 - * - * @param string $slug Font collection slug. - * @return array List of font collections. - */ - public static function get_font_collection( $slug ) { - if ( array_key_exists( $slug, self::$collections ) ) { - return self::$collections[ $slug ]; - } - return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); - } - - - - /** - * Sets the allowed mime types for fonts. - * - * @since 6.5.0 - * - * @param array $mime_types List of allowed mime types. - * @return array Modified upload directory. - */ - public static function set_allowed_mime_types( $mime_types ) { - return array_merge( $mime_types, self::get_expected_font_mime_types_per_php_version() ); - } - } -} diff --git a/lib/experimental/fonts/font-library/class-wp-font-utils.php b/lib/experimental/fonts/font-library/class-wp-font-utils.php deleted file mode 100644 index e6fdc5ae08f5d8..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-font-utils.php +++ /dev/null @@ -1,137 +0,0 @@ - '', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'fontStretch' => '100%', - 'unicodeRange' => 'U+0-10FFFF', - ) - ); - - // Convert all values to lowercase for comparison. - // Font family names may use multibyte characters. - $font_family = mb_strtolower( $settings['fontFamily'] ); - $font_style = strtolower( $settings['fontStyle'] ); - $font_weight = strtolower( $settings['fontWeight'] ); - $font_stretch = strtolower( $settings['fontStretch'] ); - $unicode_range = strtoupper( $settings['unicodeRange'] ); - - // Convert weight keywords to numeric strings. - $font_weight = str_replace( 'normal', '400', $font_weight ); - $font_weight = str_replace( 'bold', '700', $font_weight ); - - // Convert stretch keywords to numeric strings. - $font_stretch_map = array( - 'ultra-condensed' => '50%', - 'extra-condensed' => '62.5%', - 'condensed' => '75%', - 'semi-condensed' => '87.5%', - 'normal' => '100%', - 'semi-expanded' => '112.5%', - 'expanded' => '125%', - 'extra-expanded' => '150%', - 'ultra-expanded' => '200%', - ); - $font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch ); - - $slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range ); - - $slug_elements = array_map( - function ( $elem ) { - // Remove quotes to normalize font-family names, and ';' to use as a separator. - $elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) ); - - // Normalize comma separated lists by removing whitespace in between items, - // but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts). - // CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE, - // which by default are all matched by \s in PHP. - return preg_replace( '/,\s+/', ',', $elem ); - }, - $slug_elements - ); - - return join( ';', $slug_elements ); - } - } -} diff --git a/lib/load.php b/lib/load.php index 46ee3bc3681eab..b92f5b734548a0 100644 --- a/lib/load.php +++ b/lib/load.php @@ -137,13 +137,13 @@ function gutenberg_is_experiment_enabled( $name ) { remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. // Loads the Font Library. -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-collection.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-library.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-utils.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/font-library.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-collection.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-library.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-utils.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/fonts.php'; // Load the Font Face and Font Face Resolver, if not already loaded by WordPress Core. if ( ! class_exists( 'WP_Font_Face' ) ) { @@ -160,18 +160,18 @@ function gutenberg_is_experiment_enabled( $name ) { // Load the BC Layer to avoid fatal errors of extenders using the Fonts API. // @core-merge: do not merge the BC layer files into WordPress Core. -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-utils.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider-local.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-resolver.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/webfonts-deprecations.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts-utils.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider-local.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-web-fonts.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-utils.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider-local.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-resolver.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php'; +require __DIR__ . '/experimental/font-face/bc-layer/webfonts-deprecations.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts-utils.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts-provider.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts-provider-local.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-web-fonts.php'; // Plugin specific code. require __DIR__ . '/script-loader.php'; diff --git a/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php b/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php index a971bd51234305..dc720b6b7db701 100644 --- a/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php +++ b/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php @@ -6,18 +6,28 @@ * * @package WordPress * @subpackage Font Library + * + * @group fonts + * @group font-library */ class Tests_Font_Family_Backwards_Compatibility extends WP_UnitTestCase { + private $post_ids_to_delete; + public function set_up() { parent::set_up(); + $this->post_ids_to_delete = array(); delete_option( 'gutenberg_font_family_format_converted' ); } public function tear_down() { - parent::tear_down(); + foreach ( $this->post_ids_to_delete as $post_id ) { + wp_delete_post( $post_id, true ); + } delete_option( 'gutenberg_font_family_format_converted' ); + + parent::tear_down(); } public function test_font_faces_with_remote_src() { @@ -27,79 +37,74 @@ public function test_font_faces_with_remote_src() { gutenberg_convert_legacy_font_family_format(); - $font_family = get_post( $font_family_id ); + $font_family = $this->get_post( $font_family_id ); $font_faces = $this->get_font_faces( $font_family_id ); list( $font_face1, $font_face2, $font_face3 ) = $font_faces; // Updated font family post. - $this->assertSame( 'wp_font_family', $font_family->post_type ); - $this->assertSame( 'publish', $font_family->post_status ); + $this->assertSame( 'wp_font_family', $font_family->post_type, 'The font family post type should be wp_font_family.' ); + $this->assertSame( 'publish', $font_family->post_status, 'The font family post status should be publish.' ); $font_family_title = 'Open Sans'; - $this->assertSame( $font_family_title, $font_family->post_title ); + $this->assertSame( $font_family_title, $font_family->post_title, 'The font family post title should be Open Sans.' ); $font_family_slug = 'open-sans'; - $this->assertSame( $font_family_slug, $font_family->post_name ); + $this->assertSame( $font_family_slug, $font_family->post_name, 'The font family post name should be open-sans.' ); $font_family_content = wp_json_encode( json_decode( '{"fontFamily":"\'Open Sans\', sans-serif","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans.svg"}', true ) ); - $this->assertSame( $font_family_content, $font_family->post_content ); + $this->assertSame( $font_family_content, $font_family->post_content, 'The font family post content should match.' ); $meta = get_post_meta( $font_family_id, '_gutenberg_legacy_font_family', true ); - $this->assertSame( $legacy_content, $meta ); + $this->assertSame( $legacy_content, $meta, 'The _gutenberg_legacy_font_family post meta content should match.' ); // First font face post. - $this->assertSame( 'wp_font_face', $font_face1->post_type ); - $this->assertSame( $font_family_id, $font_face1->post_parent ); - $this->assertSame( 'publish', $font_face1->post_status ); + $this->assertSame( 'wp_font_face', $font_face1->post_type, 'The 1st font face post type should be wp_font_face.' ); + $this->assertSame( $font_family_id, $font_face1->post_parent, 'The 1st font face post parent should match.' ); + $this->assertSame( 'publish', $font_face1->post_status, 'The 1st font face post status should be publish.' ); $font_face1_title = 'open sans;normal;400;100%;U+0-10FFFF'; - $this->assertSame( $font_face1_title, $font_face1->post_title ); - $this->assertSame( sanitize_title( $font_face1_title ), $font_face1->post_name ); + $this->assertSame( $font_face1_title, $font_face1->post_title, 'The 1st font face post title should match.' ); + $this->assertSame( sanitize_title( $font_face1_title ), $font_face1->post_name, 'The 1st font face post name should match.' ); $font_face1_content = wp_json_encode( json_decode( '{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4nY1M2xLER.ttf"}' ) ); - $this->assertSame( $font_face1_content, $font_face1->post_content ); + $this->assertSame( $font_face1_content, $font_face1->post_content, 'The 1st font face post content should match.' ); // With a remote url, file post meta should not be set. $meta = get_post_meta( $font_face1->ID, '_wp_font_face_file', true ); - $this->assertSame( '', $meta ); + $this->assertSame( '', $meta, 'The _wp_font_face_file post meta for the 1st font face should be an empty string.' ); // Second font face post. - $this->assertSame( 'wp_font_face', $font_face2->post_type ); - $this->assertSame( $font_family_id, $font_face2->post_parent ); - $this->assertSame( 'publish', $font_face2->post_status ); + $this->assertSame( 'wp_font_face', $font_face2->post_type, 'The 2nd font face post type should be wp_font_face.' ); + $this->assertSame( $font_family_id, $font_face2->post_parent, 'The 2md font face post type should be wp_font_face.' ); + $this->assertSame( 'publish', $font_face2->post_status, 'The 2nd font face post status should be publish.' ); $font_face2_title = 'open sans;italic;400;100%;U+0-10FFFF'; - $this->assertSame( $font_face2_title, $font_face2->post_title ); - $this->assertSame( sanitize_title( $font_face2_title ), $font_face2->post_name ); + $this->assertSame( $font_face2_title, $font_face2->post_title, 'The 2nd font face post title should match.' ); + $this->assertSame( sanitize_title( $font_face2_title ), $font_face2->post_name, 'The 2nd font face post name should match.' ); $font_face2_content = wp_json_encode( json_decode( '{"fontFamily":"Open Sans","fontStyle":"italic","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-italic.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk8ZkaVcUwaERZjA.ttf"}' ) ); - $this->assertSame( $font_face2_content, $font_face2->post_content ); + $this->assertSame( $font_face2_content, $font_face2->post_content, 'The 2nd font face post content should match.' ); // With a remote url, file post meta should not be set. $meta = get_post_meta( $font_face2->ID, '_wp_font_face_file', true ); - $this->assertSame( '', $meta ); + $this->assertSame( '', $meta, 'The _wp_font_face_file post meta for the 2nd font face should be an empty string.' ); // Third font face post. - $this->assertSame( 'wp_font_face', $font_face3->post_type ); - $this->assertSame( $font_family_id, $font_face3->post_parent ); - $this->assertSame( 'publish', $font_face3->post_status ); + $this->assertSame( 'wp_font_face', $font_face3->post_type, 'The 3rd font face post type should be wp_font_face.' ); + $this->assertSame( $font_family_id, $font_face3->post_parent, 'The 3rd font face post type should be wp_font_face.' ); + $this->assertSame( 'publish', $font_face3->post_status, 'The 3rd font face post status should be publish.' ); $font_face3_title = 'open sans;normal;700;100%;U+0-10FFFF'; - $this->assertSame( $font_face3_title, $font_face3->post_title ); - $this->assertSame( sanitize_title( $font_face3_title ), $font_face3->post_name ); + $this->assertSame( $font_face3_title, $font_face3->post_title, 'The 3rd font face post title should match.' ); + $this->assertSame( sanitize_title( $font_face3_title ), $font_face3->post_name, 'The 3rd font face post name should match.' ); $font_face3_content = wp_json_encode( json_decode( '{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"700","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-700-normal.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1y4nY1M2xLER.ttf"}' ) ); - $this->assertSame( $font_face3_content, $font_face3->post_content ); + $this->assertSame( $font_face3_content, $font_face3->post_content, 'The 3rd font face post content should match.' ); // With a remote url, file post meta should not be set. $meta = get_post_meta( $font_face3->ID, '_wp_font_face_file', true ); - $this->assertSame( '', $meta ); - - wp_delete_post( $font_family_id, true ); - wp_delete_post( $font_face1->ID, true ); - wp_delete_post( $font_face2->ID, true ); - wp_delete_post( $font_face3->ID, true ); + $this->assertSame( '', $meta, 'The _wp_font_face_file post meta for the 3rd font face should be an empty string.' ); } public function test_font_faces_with_local_src() { @@ -110,16 +115,14 @@ public function test_font_faces_with_local_src() { gutenberg_convert_legacy_font_family_format(); $font_faces = $this->get_font_faces( $font_family_id ); - $this->assertCount( 1, $font_faces ); + + $this->assertCount( 1, $font_faces, 'There should be 1 font face.' ); $font_face = reset( $font_faces ); // Check that file meta is present. $file_path = 'open-sans_normal_400.ttf'; $meta = get_post_meta( $font_face->ID, '_wp_font_face_file', true ); - $this->assertSame( $file_path, $meta ); - - wp_delete_post( $font_family_id, true ); - wp_delete_post( $font_face->ID, true ); + $this->assertSame( $file_path, $meta, 'The _wp_font_face_file should match.' ); } public function test_migration_only_runs_once() { @@ -135,12 +138,10 @@ public function test_migration_only_runs_once() { // Meta with backup content will not be present if migration isn't triggered. $meta = get_post_meta( $font_family_id, '_gutenberg_legacy_font_family', true ); $this->assertSame( '', $meta ); - - wp_delete_post( $font_family_id, true ); } protected function create_font_family( $content ) { - return wp_insert_post( + $post_id = wp_insert_post( array( 'post_type' => 'wp_font_family', 'post_status' => 'publish', @@ -149,10 +150,22 @@ protected function create_font_family( $content ) { 'post_content' => $content, ) ); + + $this->store_id_for_cleanup_in_teardown( $post_id ); + + return $post_id; + } + + private function get_post( $post_id ) { + $post = get_post( $post_id ); + + $this->store_id_for_cleanup_in_teardown( $post ); + + return $post; } protected function get_font_faces( $font_family_id ) { - return get_posts( + $posts = get_posts( array( 'post_parent' => $font_family_id, 'post_type' => 'wp_font_face', @@ -160,5 +173,24 @@ protected function get_font_faces( $font_family_id ) { 'orderby' => 'id', ) ); + + $this->store_id_for_cleanup_in_teardown( $posts ); + + return $posts; + } + + private function store_id_for_cleanup_in_teardown( $post ) { + if ( null === $post ) { + return; + } + + $posts = is_array( $post ) ? $post : array( $post ); + + foreach ( $posts as $post ) { + if ( null === $post ) { + continue; + } + $this->post_ids_to_delete[] = is_int( $post ) ? $post : $post->ID; + } } } diff --git a/phpunit/tests/fonts/font-library/fontLibraryHooks.php b/phpunit/tests/fonts/font-library/fontLibraryHooks.php index 2c471e2a9759c6..85d631ecaa45f2 100644 --- a/phpunit/tests/fonts/font-library/fontLibraryHooks.php +++ b/phpunit/tests/fonts/font-library/fontLibraryHooks.php @@ -4,9 +4,12 @@ * * @package WordPress * @subpackage Font Library + * + * @group fonts + * @group font-library */ +class Tests_Fonts_FontLibraryHooks extends WP_UnitTestCase { -class Tests_Font_Library_Hooks extends WP_UnitTestCase { public function test_deleting_font_family_deletes_child_font_faces() { $font_family_id = self::factory()->post->create( array( @@ -33,8 +36,8 @@ public function test_deleting_font_family_deletes_child_font_faces() { wp_delete_post( $font_family_id, true ); - $this->assertNull( get_post( $font_face_id ) ); - $this->assertNotNull( get_post( $other_font_face_id ) ); + $this->assertNull( get_post( $font_face_id ), 'Font face post should also have been deleted.' ); + $this->assertNotNull( get_post( $other_font_face_id ), 'The other post should exist.' ); } public function test_deleting_font_faces_deletes_associated_font_files() { @@ -43,8 +46,8 @@ public function test_deleting_font_faces_deletes_associated_font_files() { wp_delete_post( $font_face_id, true ); - $this->assertFalse( file_exists( $font_path ) ); - $this->assertTrue( file_exists( $other_font_path ) ); + $this->assertFalse( file_exists( $font_path ), 'The font file should have been deleted when the post was deleted.' ); + $this->assertTrue( file_exists( $other_font_path ), 'The other font file should exist.' ); } protected function create_font_face_with_file( $filename ) { @@ -70,7 +73,7 @@ protected function upload_font_file( $font_filename ) { // @core-merge Use `DIR_TESTDATA` instead of `GUTENBERG_DIR_TESTDATA`. $font_file_path = GUTENBERG_DIR_TESTDATA . 'fonts/' . $font_filename; - add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); add_filter( 'upload_dir', 'wp_get_font_dir' ); $font_file = wp_upload_bits( $font_filename, @@ -78,7 +81,7 @@ protected function upload_font_file( $font_filename ) { file_get_contents( $font_file_path ) ); remove_filter( 'upload_dir', 'wp_get_font_dir' ); - remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); return $font_file; } diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php index d4b75038a6e7ac..a0693ce3414565 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -1,6 +1,6 @@ setAccessible( true ); - - $name = new ReflectionProperty( WP_Font_Collection::class, 'name' ); - $name->setAccessible( true ); - - $description = new ReflectionProperty( WP_Font_Collection::class, 'description' ); - $description->setAccessible( true ); - - $src = new ReflectionProperty( WP_Font_Collection::class, 'src' ); - $src->setAccessible( true ); - - $config = array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'src' => 'my-collection-data.json', + public function test_should_do_it_wrong_with_invalid_slug() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::__construct' ); + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock ' ), ); - $collection = new WP_Font_Collection( $config ); - - $actual_slug = $slug->getValue( $collection ); - $this->assertSame( 'my-collection', $actual_slug, 'Provided slug and initialized slug should match.' ); - $slug->setAccessible( false ); - - $actual_name = $name->getValue( $collection ); - $this->assertSame( 'My Collection', $actual_name, 'Provided name and initialized name should match.' ); - $name->setAccessible( false ); - - $actual_description = $description->getValue( $collection ); - $this->assertSame( 'My collection description', $actual_description, 'Provided description and initialized description should match.' ); - $description->setAccessible( false ); - - $actual_src = $src->getValue( $collection ); - $this->assertSame( 'my-collection-data.json', $actual_src, 'Provided src and initialized src should match.' ); - $src->setAccessible( false ); - } - - /** - * @dataProvider data_should_do_it_wrong - * - * @param mixed $config Config of the font collection. - */ - public function test_should_do_it_wrong( $config ) { - $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); - new WP_Font_Collection( $config ); - } - - /** - * Data provider. - * - * @return array - */ - public function data_should_do_it_wrong() { - return array( - 'no id' => array( - array( - 'name' => 'My Collection', - 'description' => 'My collection description', - 'src' => 'my-collection-data.json', - ), - ), - 'no config' => array( - '', - ), + $collection = new WP_Font_Collection( 'slug with spaces', $mock_collection_data ); - 'empty array' => array( - array(), - ), - - 'boolean instead of config array' => array( - false, - ), - - 'null instead of config array' => array( - null, - ), - - 'missing src' => array( - array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - ), - ), - ); + $this->assertSame( 'slug-with-spaces', $collection->slug, 'Slug is not sanitized.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getContent.php b/phpunit/tests/fonts/font-library/wpFontCollection/getContent.php deleted file mode 100644 index ab0e87cde000e7..00000000000000 --- a/phpunit/tests/fonts/font-library/wpFontCollection/getContent.php +++ /dev/null @@ -1,121 +0,0 @@ - array( 'mock' ), - 'categories' => array( 'mock' ), - ); - - return array( - 'body' => json_encode( $mock_collection_data ), - 'response' => array( - 'code' => 200, - ), - ); - } - - /** - * @dataProvider data_should_get_content - * - * @param array $config Font collection config options. - * @param array $expected_data Expected output data. - */ - public function test_should_get_content( $config, $expected_data ) { - $collection = new WP_Font_Collection( $config ); - $this->assertSame( $expected_data, $collection->get_content() ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_should_get_content() { - $mock_file = wp_tempnam( 'my-collection-data-' ); - file_put_contents( $mock_file, '{"font_families":[ "mock" ], "categories":[ "mock" ] }' ); - - return array( - 'with a file' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'src' => $mock_file, - ), - 'expected_data' => array( - 'font_families' => array( 'mock' ), - 'categories' => array( 'mock' ), - ), - ), - 'with a url' => array( - 'config' => array( - 'slug' => 'my-collection-with-url', - 'name' => 'My Collection with URL', - 'description' => 'My collection description', - 'src' => 'https://localhost/fonts/mock-font-collection.json', - ), - 'expected_data' => array( - 'font_families' => array( 'mock' ), - 'categories' => array( 'mock' ), - ), - ), - 'with font_families and categories' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'font_families' => array( 'mock' ), - 'categories' => array( 'mock' ), - ), - 'expected_data' => array( - 'font_families' => array( 'mock' ), - 'categories' => array( 'mock' ), - ), - ), - 'with font_families without categories' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'font_families' => array( 'mock' ), - ), - 'expected_data' => array( - 'font_families' => array( 'mock' ), - 'categories' => array(), - ), - ), - ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/phpunit/tests/fonts/font-library/wpFontCollection/getData.php new file mode 100644 index 00000000000000..1cb16e271db61a --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getData.php @@ -0,0 +1,358 @@ +get_data(); + + $this->assertSame( $slug, $collection->slug, 'The slug should match.' ); + $this->assertSame( $expected_data, $data, 'The collection data should match.' ); + } + + /** + * @dataProvider data_create_font_collection + * + * @param string $slug Font collection slug. + * @param array $config Font collection config. + * @param array $expected_data Expected collection data. + */ + public function test_should_get_data_from_json_file( $slug, $config, $expected_data ) { + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, wp_json_encode( $config ) ); + + $collection = new WP_Font_Collection( $slug, $mock_file ); + $data = $collection->get_data(); + + $this->assertSame( $slug, $collection->slug, 'The slug should match.' ); + $this->assertSame( $expected_data, $data, 'The collection data should match.' ); + } + + /** + * @dataProvider data_create_font_collection + * + * @param string $slug Font collection slug. + * @param array $config Font collection config. + * @param array $expected_data Expected collection data. + */ + public function test_should_get_data_from_json_url( $slug, $config, $expected_data ) { + add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 ); + + self::$mock_collection_data = $config; + $collection = new WP_Font_Collection( $slug, 'https://localhost/fonts/mock-font-collection.json' ); + $data = $collection->get_data(); + + remove_filter( 'pre_http_request', array( $this, 'mock_request' ) ); + + $this->assertSame( $slug, $collection->slug, 'The slug should match.' ); + $this->assertSame( $expected_data, $data, 'The collection data should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_font_collection() { + return array( + + 'font collection with required data' => array( + 'slug' => 'my-collection', + 'config' => array( + 'name' => 'My Collection', + 'font_families' => array( array() ), + ), + 'expected_data' => array( + 'description' => '', + 'categories' => array(), + 'name' => 'My Collection', + 'font_families' => array( array() ), + ), + ), + + 'font collection with all data' => array( + 'slug' => 'my-collection', + 'config' => array( + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( array() ), + 'categories' => array(), + ), + 'expected_data' => array( + 'description' => 'My collection description', + 'categories' => array(), + 'name' => 'My Collection', + 'font_families' => array( array() ), + ), + ), + + 'font collection with risky data' => array( + 'slug' => 'my-collection', + 'config' => array( + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( + array( + 'font_family_settings' => array( + 'fontFamily' => 'Open Sans, sans-serif', + 'slug' => 'open-sans', + 'name' => 'Open Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'https://example.com/src-as-string.ttf?a=', + ), + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'https://example.com/src-as-array.woff2?a=', + 'https://example.com/src-as-array.ttf', + ), + ), + ), + 'unwanted_property' => 'potentially evil value', + ), + 'categories' => array( 'sans-serif' ), + ), + ), + 'categories' => array( + array( + 'name' => 'Mock col', + 'slug' => 'mock-col', + 'unwanted_property' => 'potentially evil value', + ), + ), + 'unwanted_property' => 'potentially evil value', + ), + 'expected_data' => array( + 'description' => 'My collection description', + 'categories' => array( + array( + 'name' => 'Mock col', + 'slug' => 'mock-colalertxss', + ), + ), + 'name' => 'My Collection', + 'font_families' => array( + array( + 'font_family_settings' => array( + 'fontFamily' => 'Open Sans, sans-serif', + 'slug' => 'open-sans', + 'name' => 'Open Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'https://example.com/src-as-string.ttf?a=', + ), + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'https://example.com/src-as-array.woff2?a=', + 'https://example.com/src-as-array.ttf', + ), + ), + ), + ), + 'categories' => array( 'sans-serifalertxss' ), + ), + ), + ), + ), + + ); + } + + /** + * @dataProvider data_should_error_when_missing_properties + * + * @param array $config Font collection config. + */ + public function test_should_error_when_missing_properties( $config ) { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::sanitize_and_validate_data' ); + + $collection = new WP_Font_Collection( 'my-collection', $config ); + $data = $collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned when property is missing or invalid.' ); + $this->assertSame( + $data->get_error_code(), + 'font_collection_missing_property', + 'Incorrect error code when property is missing or invalid.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_error_when_missing_properties() { + return array( + 'missing name' => array( + 'config' => array( + 'font_families' => array( 'mock' ), + ), + ), + 'empty name' => array( + 'config' => array( + 'name' => '', + 'font_families' => array( 'mock' ), + ), + ), + 'missing font_families' => array( + 'config' => array( + 'name' => 'My Collection', + ), + ), + 'empty font_families' => array( + 'config' => array( + 'name' => 'My Collection', + 'font_families' => array(), + ), + ), + ); + } + + public function test_should_error_with_invalid_json_file_path() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + $collection = new WP_Font_Collection( 'my-collection', 'non-existing.json' ); + $data = $collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned when invalid file path is provided.' ); + $this->assertSame( + $data->get_error_code(), + 'font_collection_json_missing', + 'Incorrect error code when invalid file path is provided.' + ); + } + + public function test_should_error_with_invalid_json_from_file() { + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, 'invalid-json' ); + + $collection = new WP_Font_Collection( 'my-collection', $mock_file ); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Testing error response returned by `load_from_json`, not the underlying error from `wp_json_file_decode`. + $data = @$collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned with invalid json file contents.' ); + $this->assertSame( + $data->get_error_code(), + 'font_collection_decode_error', + 'Incorrect error code with invalid json file contents.' + ); + } + + public function test_should_error_with_invalid_url() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + $collection = new WP_Font_Collection( 'my-collection', 'not-a-url' ); + $data = $collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned when invalid url is provided.' ); + $this->assertSame( + $data->get_error_code(), + 'font_collection_json_missing', + 'Incorrect error code when invalid url is provided.' + ); + } + + public function test_should_error_with_unsuccessful_response_status() { + add_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ), 10, 3 ); + + $collection = new WP_Font_Collection( 'my-collection', 'https://localhost/fonts/missing-collection.json' ); + $data = $collection->get_data(); + + remove_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ) ); + + $this->assertWPError( $data, 'Error is not returned when response is unsuccessful.' ); + $this->assertSame( + $data->get_error_code(), + 'font_collection_request_error', + 'Incorrect error code when response is unsuccussful.' + ); + } + + public function test_should_error_with_invalid_json_from_url() { + add_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ), 10, 3 ); + + $collection = new WP_Font_Collection( 'my-collection', 'https://localhost/fonts/invalid-collection.json' ); + $data = $collection->get_data(); + + remove_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ) ); + + $this->assertWPError( $data, 'Error is not returned when response is invalid json.' ); + $this->assertSame( + $data->get_error_code(), + 'font_collection_decode_error', + 'Incorrect error code when response is invalid json.' + ); + } + + public function mock_request( $preempt, $args, $url ) { + if ( 'https://localhost/fonts/mock-font-collection.json' !== $url ) { + return false; + } + + return array( + 'body' => wp_json_encode( self::$mock_collection_data ), + 'response' => array( + 'code' => 200, + ), + ); + } + + public function mock_request_unsuccessful_response( $preempt, $args, $url ) { + if ( 'https://localhost/fonts/missing-collection.json' !== $url ) { + return false; + } + + return array( + 'body' => '', + 'response' => array( + 'code' => 404, + ), + ); + } + + public function mock_request_invalid_json( $preempt, $args, $url ) { + if ( 'https://localhost/fonts/invalid-collection.json' !== $url ) { + return false; + } + + return array( + 'body' => 'invalid', + 'response' => array( + 'code' => 200, + ), + ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php b/phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php deleted file mode 100644 index 9179db9db10a8a..00000000000000 --- a/phpunit/tests/fonts/font-library/wpFontCollection/isConfigValid.php +++ /dev/null @@ -1,103 +0,0 @@ -assertTrue( WP_Font_Collection::is_config_valid( $config ) ); - } - - public function data_is_config_valid() { - return array( - 'with src' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'src' => 'my-collection-data.json', - ), - ), - 'with font families' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'font_families' => array( 'mock' ), - ), - ), - - ); - } - - /** - * @dataProvider data_is_config_valid_should_call_doing_it_wrong - * - * @param mixed $config Config of the font collection. - */ - public function test_is_config_valid_should_call_doing_it_wrong( $config ) { - $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid', 'Should call _doing_it_wrong if the config is not valid.' ); - $this->assertFalse( WP_Font_Collection::is_config_valid( $config ), 'Should return false if the config is not valid.' ); - } - - public function data_is_config_valid_should_call_doing_it_wrong() { - return array( - 'with missing slug' => array( - 'config' => array( - 'name' => 'My Collection', - 'description' => 'My collection description', - 'src' => 'my-collection-data.json', - ), - ), - 'with missing name' => array( - 'config' => array( - 'slug' => 'my-collection', - 'description' => 'My collection description', - 'src' => 'my-collection-data.json', - ), - ), - 'with missing src' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - ), - ), - 'with both src and font_families' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - 'src' => 'my-collection-data.json', - 'font_families' => array( 'mock' ), - ), - ), - 'without src or font_families' => array( - 'config' => array( - 'slug' => 'my-collection', - 'name' => 'My Collection', - 'description' => 'My collection description', - ), - ), - 'with empty config' => array( - 'config' => array(), - ), - 'without an array' => array( - 'config' => 'not an array', - ), - ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php index e8d970f5b3d393..135329e5add73a 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/base.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -7,11 +7,10 @@ */ abstract class WP_Font_Library_UnitTestCase extends WP_UnitTestCase { public function reset_font_collections() { - // Resets the private static property WP_Font_Library::$collections to empty array. - $reflection = new ReflectionClass( 'WP_Font_Library' ); - $property = $reflection->getProperty( 'collections' ); - $property->setAccessible( true ); - $property->setValue( array() ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); + foreach ( $collections as $slug => $collection ) { + WP_Font_Library::get_instance()->unregister_font_collection( $slug ); + } } public function set_up() { diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php index 082ca892114659..675efe81aec59b 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -13,19 +13,18 @@ class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTestCase { public function test_should_get_font_collection() { - $my_font_collection_config = array( - 'slug' => 'my-font-collection', - 'name' => 'My Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), ); - wp_register_font_collection( $my_font_collection_config ); - $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' ); + + wp_register_font_collection( 'my-font-collection', $mock_collection_data ); + $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'my-font-collection' ); $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); } public function test_should_get_no_font_collection_if_the_slug_is_not_registered() { - $font_collection = WP_Font_Library::get_font_collection( 'not-registered-font-collection' ); + $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'not-registered-font-collection' ); $this->assertWPError( $font_collection ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php index a405584efccc23..f5ca6389b8ff51 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -12,22 +12,21 @@ */ class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_Font_Library_UnitTestCase { public function test_should_get_an_empty_list() { - $font_collections = WP_Font_Library::get_font_collections(); + $font_collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertEmpty( $font_collections, 'Should return an empty array.' ); } public function test_should_get_mock_font_collection() { $my_font_collection_config = array( - 'slug' => 'my-font-collection', - 'name' => 'My Font Collection', - 'description' => 'Demo about how to a font collection to your WordPress Font Library.', - 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), + 'name' => 'My Font Collection', + 'description' => 'Demo about how to a font collection to your WordPress Font Library.', + 'font_families' => array( 'mock' ), ); - WP_Font_Library::register_font_collection( $my_font_collection_config ); + WP_Font_Library::get_instance()->register_font_collection( 'my-font-collection', $my_font_collection_config ); - $font_collections = WP_Font_Library::get_font_collections(); - $this->assertNotEmpty( $font_collections, 'Sould return an array of font collections.' ); + $font_collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertNotEmpty( $font_collections, 'Should return an array of font collections.' ); $this->assertCount( 1, $font_collections, 'Should return an array with one font collection.' ); $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/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php deleted file mode 100644 index 579baa2d248e0b..00000000000000 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ /dev/null @@ -1,90 +0,0 @@ -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' => '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', - ), - ), - ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index b06ae3c8d53548..d3b0f126e2e7e1 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -11,69 +11,30 @@ * @covers WP_Font_Library::register_font_collection */ class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_UnitTestCase { - public function test_should_register_font_collection() { - $config = array( - 'slug' => '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_slug_is_missing() { $config = array( - 'name' => 'My Collection', - 'description' => 'My Collection Description', - 'src' => 'my-collection-data.json', + 'name' => 'My Collection', + 'font_families' => array( 'mock' ), ); - $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); - $collection = WP_Font_Library::register_font_collection( $config ); - $this->assertWPError( $collection, 'A WP_Error should be returned.' ); - } - public function test_should_return_error_if_name_is_missing() { - $config = array( - 'slug' => 'my-collection', - 'description' => 'My Collection Description', - 'src' => 'my-collection-data.json', - ); - $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); - $collection = WP_Font_Library::register_font_collection( $config ); - $this->assertWPError( $collection, 'A WP_Error should be returned.' ); - } - - public function test_should_return_error_if_config_is_empty() { - $config = array(); - $this->setExpectedIncorrectUsage( 'WP_Font_Collection::is_config_valid' ); - $collection = WP_Font_Library::register_font_collection( $config ); - $this->assertWPError( $collection, 'A WP_Error should be returned.' ); + $collection = WP_Font_Library::get_instance()->register_font_collection( 'my-collection', $config ); + $this->assertInstanceOf( 'WP_Font_Collection', $collection ); } public function test_should_return_error_if_slug_is_repeated() { - $config1 = array( - 'slug' => 'my-collection-1', - 'name' => 'My Collection 1', - 'description' => 'My Collection 1 Description', - 'src' => 'my-collection-1-data.json', - ); - $config2 = array( - 'slug' => 'my-collection-1', - 'name' => 'My Collection 2', - 'description' => 'My Collection 2 Description', - 'src' => 'my-collection-2-data.json', + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), ); // Register first collection. - $collection1 = WP_Font_Library::register_font_collection( $config1 ); + $collection1 = WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data ); $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' ); // Expects a _doing_it_wrong notice. $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' ); + // Try to register a second collection with same slug. - $collection2 = WP_Font_Library::register_font_collection( $config2 ); - $this->assertWPError( $collection2, 'A WP_Error should be returned.' ); + WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php index 3c19a1d2089e7a..ddb0fa91c1d609 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -13,32 +13,24 @@ class Tests_Fonts_WpFontLibrary_UnregisterFontCollection extends WP_Font_Library_UnitTestCase { public function test_should_unregister_font_collection() { - // Registers two mock font collections. - $config = array( - 'slug' => 'mock-font-collection-1', - 'name' => 'Mock Collection to be unregistered', - 'description' => 'A mock font collection to be unregistered.', - 'src' => 'my-collection-data.json', + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), ); - WP_Font_Library::register_font_collection( $config ); - $config = array( - 'slug' => 'mock-font-collection-2', - 'name' => 'Mock Collection', - 'description' => 'A mock font collection.', - 'src' => 'my-mock-data.json', - ); - WP_Font_Library::register_font_collection( $config ); + // Registers two mock font collections. + WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-1', $mock_collection_data ); + WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-2', $mock_collection_data ); // Unregister mock font collection. - WP_Font_Library::unregister_font_collection( 'mock-font-collection-1' ); - $collections = WP_Font_Library::get_font_collections(); + WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-1' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertArrayNotHasKey( 'mock-font-collection-1', $collections, 'Font collection was not unregistered.' ); $this->assertArrayHasKey( 'mock-font-collection-2', $collections, 'Font collection was unregistered by mistake.' ); // Unregisters remaining mock font collection. - WP_Font_Library::unregister_font_collection( 'mock-font-collection-2' ); - $collections = WP_Font_Library::get_font_collections(); + WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-2' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' ); // Checks that all font collections were unregistered. @@ -46,9 +38,9 @@ public function test_should_unregister_font_collection() { } public function unregister_non_existing_collection() { - // Unregisters non existing font collection. - WP_Font_Library::unregister_font_collection( 'non-existing-collection' ); - $collections = WP_Font_Library::get_font_collections(); - $this->assertEmpty( $collections, 'Should not be registered collections.' ); + // Unregisters non-existing font collection. + WP_Font_Library::get_instance()->unregister_font_collection( 'non-existing-collection' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertEmpty( $collections, 'No collections should be registered.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php b/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php index ded19749b9123e..de0b02e63185ed 100644 --- a/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php +++ b/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php @@ -4,12 +4,18 @@ * * @package WordPress * @subpackage Font Library - * * + * + * @group fonts + * @group font-library + * * @covers WP_Font_Utils::get_font_face_slug */ class Tests_Fonts_WpFontUtils_GetFontFaceSlug extends WP_UnitTestCase { /** * @dataProvider data_get_font_face_slug_normalizes_values + * + * @param string[] $settings Settings to test. + * @param string $expected_slug Expected slug results. */ public function test_get_font_face_slug_normalizes_values( $settings, $expected_slug ) { $slug = WP_Font_Utils::get_font_face_slug( $settings ); @@ -17,6 +23,11 @@ public function test_get_font_face_slug_normalizes_values( $settings, $expected_ $this->assertSame( $expected_slug, $slug ); } + /** + * Data provider. + * + * @return array + */ public function data_get_font_face_slug_normalizes_values() { return array( 'Sets defaults' => array( diff --git a/phpunit/tests/fonts/font-library/wpFontUtils/formatFontFamily.php b/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php similarity index 58% rename from phpunit/tests/fonts/font-library/wpFontUtils/formatFontFamily.php rename to phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php index 53eaced4875207..71511331c65dcb 100644 --- a/phpunit/tests/fonts/font-library/wpFontUtils/formatFontFamily.php +++ b/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php @@ -1,6 +1,6 @@ assertSame( $expected, - WP_Font_Utils::format_font_family( + WP_Font_Utils::sanitize_font_family( $font_family ) ); @@ -30,9 +30,9 @@ public function test_should_format_font_family( $font_family, $expected ) { /** * Data provider. * - * @return array[] + * @return array */ - public function data_should_format_font_family() { + public function data_should_sanitize_font_family() { return array( 'data_families_with_spaces_and_numbers' => array( 'font_family' => 'Rock 3D , Open Sans,serif', @@ -54,6 +54,10 @@ public function data_should_format_font_family() { 'font_family' => ' ', 'expected' => '', ), + 'data_font_family_with_whitespace_tags_new_lines' => array( + 'font_family' => " Rock 3D\n ", + 'expected' => '"Rock 3D"', + ), ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php b/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php new file mode 100644 index 00000000000000..88983fe15a14ec --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php @@ -0,0 +1,310 @@ +assertSame( $result, $expected ); + } + + public function data_sanitize_from_schema() { + return array( + 'One level associative array' => array( + 'data' => array( + 'slug' => 'open - sans', + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json', + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'fontFamily' => 'sanitize_text_field', + 'src' => 'sanitize_url', + ), + 'expected' => array( + 'slug' => 'open-sansalertxss', + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + ), + + 'Nested associative arrays' => array( + 'data' => array( + 'slug' => 'open - sans', + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json', + 'nested' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nested2' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'fontFamily' => 'sanitize_text_field', + 'src' => 'sanitize_url', + 'nested' => array( + 'key1' => 'sanitize_text_field', + 'key2' => 'sanitize_text_field', + 'nested2' => array( + 'key3' => 'sanitize_text_field', + 'key4' => 'sanitize_text_field', + ), + ), + ), + 'expected' => array( + 'slug' => 'open-sansalertxss', + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + 'nested' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nested2' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + ), + ), + + 'Indexed arrays' => array( + 'data' => array( + 'slug' => 'oPeN SaNs', + 'enum' => array( + 'value1', + 'value2', + 'value3', + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'enum' => array( 'sanitize_text_field' ), + ), + 'expected' => array( + 'slug' => 'open-sans', + 'enum' => array( 'value1', 'value2', 'value3' ), + ), + ), + + 'Nested indexed arrays' => array( + 'data' => array( + 'slug' => 'OPEN-SANS', + 'name' => 'Open Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + array( + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'name' => 'sanitize_text_field', + 'fontFace' => array( + array( + 'fontFamily' => 'sanitize_text_field', + 'src' => 'sanitize_url', + ), + ), + ), + 'expected' => array( + 'slug' => 'open-sans', + 'name' => 'Open Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + array( + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + ), + ), + ), + + 'Custom sanitization function' => array( + 'data' => array( + 'key1' => 'abc123edf456ghi789', + 'key2' => 'value2', + ), + 'schema' => array( + 'key1' => function ( $value ) { + // Remove the six first character. + return substr( $value, 6 ); + }, + 'key2' => function ( $value ) { + // Capitalize the value. + return strtoupper( $value ); + }, + ), + 'expected' => array( + 'key1' => 'edf456ghi789', + 'key2' => 'VALUE2', + ), + ), + + 'Null as schema value' => array( + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nested' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + 'schema' => array( + 'key1' => null, + 'key2' => 'sanitize_text_field', + 'nested' => null, + ), + 'expected' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nested' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + ), + + 'Keys to remove' => array( + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'unwanted1' => 'value', + 'unwanted2' => 'value', + 'nestedAssociative' => array( + 'key5' => 'value5', + 'unwanted3' => 'value', + ), + 'nestedIndexed' => array( + array( + 'key6' => 'value7', + 'unwanted4' => 'value', + ), + array( + 'key6' => 'value7', + 'unwanted5' => 'value', + ), + ), + + ), + 'schema' => array( + 'key1' => 'sanitize_text_field', + 'key2' => 'sanitize_text_field', + 'nestedAssociative' => array( + 'key5' => 'sanitize_text_field', + ), + 'nestedIndexed' => array( + array( + 'key6' => 'sanitize_text_field', + ), + ), + ), + 'expected' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nestedAssociative' => array( + 'key5' => 'value5', + ), + 'nestedIndexed' => array( + array( + 'key6' => 'value7', + ), + array( + 'key6' => 'value7', + ), + ), + ), + ), + + 'With empty structure' => array( + 'data' => array( + 'slug' => 'open-sans', + 'nested' => array( + 'key1' => 'value', + 'nested2' => array( + 'key2' => 'value', + 'nested3' => array( + 'nested4' => array(), + ), + ), + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'nested' => array( + 'key1' => 'sanitize_text_field', + 'nested2' => array( + 'key2' => 'sanitize_text_field', + 'nested3' => array( + 'key3' => 'sanitize_text_field', + 'nested4' => array( + 'key4' => 'sanitize_text_field', + ), + ), + ), + ), + ), + 'expected' => array( + 'slug' => 'open-sans', + 'nested' => array( + 'key1' => 'value', + 'nested2' => array( + 'key2' => 'value', + ), + ), + ), + ), + ); + } + + public function test_sanitize_from_schema_with_invalid_data() { + $data = 'invalid data'; + $schema = array( + 'key1' => 'sanitize_text_field', + 'key2' => 'sanitize_text_field', + ); + + $result = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + $this->assertSame( $result, array() ); + } + + + public function test_sanitize_from_schema_with_invalid_schema() { + $data = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + $schema = 'invalid schema'; + + $result = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + $this->assertSame( $result, array() ); + } +} diff --git a/phpunit/tests/fonts/font-library/fontsDir.php b/phpunit/tests/fonts/font-library/wpFontsDir.php similarity index 80% rename from phpunit/tests/fonts/font-library/fontsDir.php rename to phpunit/tests/fonts/font-library/wpFontsDir.php index 5c13f1d120f9a5..a8f79888315bdf 100644 --- a/phpunit/tests/fonts/font-library/fontsDir.php +++ b/phpunit/tests/fonts/font-library/wpFontsDir.php @@ -8,14 +8,15 @@ * @group fonts * @group font-library * - * @covers wp_get_font_dir + * @covers ::wp_get_font_dir */ class Tests_Fonts_WpFontDir extends WP_UnitTestCase { - private $dir_defaults; + private static $dir_defaults; - public function __construct() { - parent::__construct(); - $this->dir_defaults = array( + public static function set_up_before_class() { + parent::set_up_before_class(); + + static::$dir_defaults = array( 'path' => path_join( WP_CONTENT_DIR, 'fonts' ), 'url' => content_url( 'fonts' ), 'subdir' => '', @@ -27,7 +28,8 @@ public function __construct() { public function test_fonts_dir() { $font_dir = wp_get_font_dir(); - $this->assertEquals( $font_dir, $this->dir_defaults ); + + $this->assertSame( $font_dir, static::$dir_defaults ); } public function test_fonts_dir_with_filter() { @@ -57,14 +59,14 @@ function set_new_values( $defaults ) { 'error' => false, ); - $this->assertEquals( $font_dir, $expected, 'The wp_get_font_dir() method should return the expected values.' ); - // Remove the filter. remove_filter( 'font_dir', 'set_new_values' ); + $this->assertSame( $expected, $font_dir, 'The wp_get_font_dir() method should return the expected values.' ); + // Gets the fonts dir. $font_dir = wp_get_font_dir(); - $this->assertEquals( $font_dir, $this->dir_defaults, 'The wp_get_font_dir() method should return the default values.' ); + $this->assertSame( static::$dir_defaults, $font_dir, 'The wp_get_font_dir() method should return the default values.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index f2140267672d2b..60f50e503fdbe4 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -7,10 +7,12 @@ * @since 6.5.0 * * @group restapi + * @group fonts + * @group font-library * * @coversDefaultClass WP_REST_Font_Collections_Controller */ -class WP_REST_Font_Collections_Controller_Test extends WP_Test_REST_Controller_Testcase { +class Tests_REST_WpRestFontCollectionsController extends WP_Test_REST_Controller_Testcase { protected static $admin_id; protected static $editor_id; protected static $mock_file; @@ -28,15 +30,9 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); $mock_file = wp_tempnam( 'my-collection-data-' ); - file_put_contents( $mock_file, '{"font_families": [ "mock" ], "categories": [ "mock" ] }' ); + file_put_contents( $mock_file, '{"name": "Mock Collection", "font_families": [ "mock" ], "categories": [ "mock" ] }' ); - wp_register_font_collection( - array( - 'name' => 'My Collection', - 'slug' => 'mock-col-slug', - 'src' => $mock_file, - ) - ); + wp_register_font_collection( 'mock-col-slug', $mock_file ); } public static function wpTearDownAfterClass() { @@ -45,7 +41,6 @@ public static function wpTearDownAfterClass() { wp_unregister_font_collection( 'mock-col-slug' ); } - /** * @covers WP_REST_Font_Collections_Controller::register_routes */ @@ -54,8 +49,8 @@ public function test_register_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.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections initialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection initialized.' ); } /** @@ -67,7 +62,26 @@ public function test_get_items() { $response = rest_get_server()->dispatch( $request ); $content = $response->get_data(); $this->assertIsArray( $content ); - $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @covers WP_REST_Font_Collections_Controller::get_items + */ + public function test_get_items_should_only_return_valid_collections() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + wp_set_current_user( self::$admin_id ); + wp_register_font_collection( 'invalid-collection', 'invalid-collection-file' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); + $response = rest_get_server()->dispatch( $request ); + $content = $response->get_data(); + + wp_unregister_font_collection( 'invalid-collection' ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertCount( 1, $content, 'The response should only contain valid collections.' ); } /** @@ -77,7 +91,7 @@ public function test_get_item() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/mock-col-slug' ); $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status(), 'Response code is not 200' ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $response_data = $response->get_data(); $this->assertArrayHasKey( 'name', $response_data, 'Response data does not have the name key.' ); @@ -104,6 +118,24 @@ public function test_get_item_invalid_slug() { $this->assertErrorResponse( 'font_collection_not_found', $response, 404 ); } + /** + * @covers WP_REST_Font_Collections_Controller::get_item + */ + public function test_get_item_invalid_collection() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + wp_set_current_user( self::$admin_id ); + $slug = 'invalid-collection'; + wp_register_font_collection( $slug, 'invalid-collection-file' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/' . $slug ); + $response = rest_get_server()->dispatch( $request ); + + wp_unregister_font_collection( $slug ); + + $this->assertErrorResponse( 'font_collection_json_missing', $response, 500, 'When the collection json file is invalid, the response should return an error for "font_collection_json_missing" with 500 status.' ); + } + /** * @covers WP_REST_Font_Collections_Controller::get_item */ @@ -112,11 +144,11 @@ public function test_get_item_invalid_id_permission() { wp_set_current_user( 0 ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'Response code should be 401 for non-authenticated users.' ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response status should be 401 for non-authenticated users.' ); wp_set_current_user( self::$editor_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'Response code should be 403 for users without the right permissions.' ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response status should be 403 for users without the right permissions.' ); } /** @@ -159,13 +191,13 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $properties = $data['schema']['properties']; - $this->assertCount( 5, $properties ); - $this->assertArrayHasKey( 'slug', $properties ); - $this->assertArrayHasKey( 'name', $properties ); - $this->assertArrayHasKey( 'description', $properties ); - $this->assertArrayHasKey( 'font_families', $properties ); - $this->assertArrayHasKey( 'categories', $properties ); + $this->assertCount( 5, $properties, 'There should be 5 properties in the response data schema.' ); + $this->assertArrayHasKey( 'slug', $properties, 'The slug property should exist in the response data schema.' ); + $this->assertArrayHasKey( 'name', $properties, 'The name property should exist in the response data schema.' ); + $this->assertArrayHasKey( 'description', $properties, 'The description property should exist in the response data schema.' ); + $this->assertArrayHasKey( 'font_families', $properties, 'The slug font_families should exist in the response data schema.' ); + $this->assertArrayHasKey( 'categories', $properties, 'The categories property should exist in the response data schema.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index d6a95814b205a2..273862e3047e88 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -7,10 +7,12 @@ * @since 6.5.0 * * @group restapi + * @group fonts + * @group font-library * - * @coversDefaultClass WP_REST_Font_Faces_Controller_Test + * @coversDefaultClass WP_REST_Font_Faces_Controller */ -class WP_REST_Font_Faces_Controller_Test extends WP_Test_REST_Controller_Testcase { +class Tests_REST_WpRestFontFacesController extends WP_Test_REST_Controller_Testcase { protected static $admin_id; protected static $editor_id; @@ -20,6 +22,8 @@ class WP_REST_Font_Faces_Controller_Test extends WP_Test_REST_Controller_Testcas protected static $font_face_id1; protected static $font_face_id2; + private static $post_ids_for_cleanup = array(); + protected static $default_settings = array( 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', @@ -28,8 +32,8 @@ class WP_REST_Font_Faces_Controller_Test extends WP_Test_REST_Controller_Testcas ); public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); - self::$other_font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); + self::$font_family_id = Tests_REST_WpRestFontFamiliesController::create_font_family_post(); + self::$other_font_family_id = Tests_REST_WpRestFontFamiliesController::create_font_family_post(); self::$font_face_id1 = self::create_font_face_post( self::$font_family_id, @@ -60,17 +64,32 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'role' => 'editor', ) ); + + self::$post_ids_for_cleanup = array(); } public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); self::delete_user( self::$editor_id ); + + wp_delete_post( self::$font_family_id, true ); + wp_delete_post( self::$other_font_family_id, true ); + wp_delete_post( self::$font_face_id1, true ); + wp_delete_post( self::$font_face_id2, true ); + } + + public function tear_down() { + foreach ( self::$post_ids_for_cleanup as $post_id ) { + wp_delete_post( $post_id, true ); + } + self::$post_ids_for_cleanup = array(); + parent::tear_down(); } public static function create_font_face_post( $parent_id, $settings = array() ) { $settings = array_merge( self::$default_settings, $settings ); $title = WP_Font_Utils::get_font_face_slug( $settings ); - return self::factory()->post->create( + $post_id = self::factory()->post->create( wp_slash( array( 'post_type' => 'wp_font_face', @@ -82,6 +101,10 @@ public static function create_font_face_post( $parent_id, $settings = array() ) ) ) ); + + self::$post_ids_for_cleanup[] = $post_id; + + return $post_id; } /** @@ -128,24 +151,45 @@ public function test_font_faces_no_autosave_routes() { } /** - * @covers WP_REST_Font_Faces_Controller::get_context_param + * @doesNotPerformAssertions */ public function test_context_param() { - // Collection. - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); - $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + // See test_get_context_param(). + } - // Single. - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); + /** + * @dataProvider data_get_context_param + * + * @covers WP_REST_Font_Faces_Controller::get_context_param + * + * @param bool $single_route Whether to test a single route. + */ + public function test_get_context_param( $single_route ) { + $route = '/wp/v2/font-families/' . self::$font_family_id . '/font-faces'; + if ( $single_route ) { + $route .= '/' . self::$font_face_id1; + } + + $request = new WP_REST_Request( 'OPTIONS', $route ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); - $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + + $endpoint_data = $data['endpoints'][0]; + $this->assertArrayNotHasKey( 'allow_batch', $endpoint_data, 'The allow_batch property should not exist in the endpoint data.' ); + $this->assertSame( 'view', $endpoint_data['args']['context']['default'], 'The endpoint\'s args::context::default should be set to view.' ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $endpoint_data['args']['context']['enum'], 'The endpoint\'s args::context::enum should be set to [ view, embed, edit ].' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_context_param() { + return array( + 'Collection' => array( false ), + 'Single' => array( true ), + ); } /** @@ -157,11 +201,11 @@ public function test_get_items() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertCount( 2, $data ); - $this->assertArrayHasKey( '_links', $data[0] ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200' ); + $this->assertCount( 2, $data, 'There should be 2 properties in the response data.' ); + $this->assertArrayHasKey( '_links', $data[0], 'The _links property should exist in the response data 0.' ); $this->check_font_face_data( $data[0], self::$font_face_id2, $data[0]['_links'] ); - $this->assertArrayHasKey( '_links', $data[1] ); + $this->assertArrayHasKey( '_links', $data[1], 'The _links property should exist in the response data 1.' ); $this->check_font_face_data( $data[1], self::$font_face_id1, $data[1]['_links'] ); } @@ -172,11 +216,11 @@ public function test_get_items_no_permission() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); wp_set_current_user( self::$editor_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); } /** @@ -198,7 +242,7 @@ public function test_get_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $this->check_font_face_data( $data, self::$font_face_id1, $response->get_links() ); } @@ -213,8 +257,9 @@ public function test_get_item_removes_extra_settings() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertArrayNotHasKey( 'extra', $data['font_face_settings'] ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' ); + $this->assertArrayNotHasKey( 'extra', $data['font_face_settings'], 'The extra property should exist in the font_face_settings data.' ); } /** @@ -230,6 +275,8 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { ) ); + self::$post_ids_for_cleanup[] = $font_face_id; + $empty_settings = array( 'fontFamily' => '', 'src' => array(), @@ -240,10 +287,9 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertSame( $empty_settings, $data['font_face_settings'] ); - - wp_delete_post( $font_face_id, true ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' ); + $this->assertSame( $empty_settings, $data['font_face_settings'], 'The empty settings should exist in the font_face_settings data.' ); } /** @@ -264,11 +310,11 @@ public function test_get_item_no_permission() { $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); wp_set_current_user( self::$editor_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); } /** @@ -291,7 +337,7 @@ public function test_get_item_valid_parent_id() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); } @@ -333,24 +379,23 @@ public function test_create_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 201, $response->get_status() ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); $this->check_file_meta( $data['id'], array( $data['font_face_settings']['src'] ) ); $settings = $data['font_face_settings']; unset( $settings['src'] ); $this->assertSame( - $settings, array( 'fontFamily' => '"Open Sans"', 'fontWeight' => '200', 'fontStyle' => 'normal', - ) + ), + $settings, + 'The font_face_settings data should match the expected data.' ); $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); - - wp_delete_post( $data['id'], true ); } /** @@ -378,14 +423,12 @@ public function test_create_item_with_multiple_font_files() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 201, $response->get_status() ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); $this->check_file_meta( $data['id'], $data['font_face_settings']['src'] ); $settings = $data['font_face_settings']; - $this->assertCount( 4, $settings['src'] ); - - wp_delete_post( $data['id'], true ); + $this->assertCount( 4, $settings['src'], 'There should be 4 items in the font_face_settings::src data.' ); } /** @@ -451,10 +494,8 @@ public function test_create_item_with_url_src() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 201, $response->get_status() ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); - - wp_delete_post( $data['id'], true ); } /** @@ -487,12 +528,11 @@ public function test_create_item_with_all_properties() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - - $this->assertSame( 201, $response->get_status() ); - $this->assertArrayHasKey( 'font_face_settings', $data ); - $this->assertSame( $properties, $data['font_face_settings'] ); - wp_delete_post( $data['id'], true ); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' ); + $this->assertSame( $properties, $data['font_face_settings'], 'The font_face_settings should match the expected properties.' ); } /** @@ -514,13 +554,13 @@ public function test_create_item_missing_parent() { * @covers WP_REST_Font_Faces_Controller::create_item */ public function test_create_item_with_duplicate_properties() { - $settings = array( + $settings = array( 'fontFamily' => '"Open Sans"', 'fontWeight' => '200', 'fontStyle' => 'italic', 'src' => home_url( '/wp-content/fonts/open-sans-italic-light.ttf' ), ); - $font_face_id = self::create_font_face_post( self::$font_family_id, $settings ); + self::create_font_face_post( self::$font_family_id, $settings ); wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); @@ -528,12 +568,10 @@ public function test_create_item_with_duplicate_properties() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_duplicate_font_face', $response, 400 ); + $this->assertErrorResponse( 'rest_duplicate_font_face', $response, 400, 'The response should return an error for "rest_duplicate_font_face" with 400 status.' ); $expected_message = 'A font face matching those settings already exists.'; $message = $response->as_error()->get_error_messages()[0]; - $this->assertSame( $expected_message, $message ); - - wp_delete_post( $font_face_id, true ); + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** @@ -555,18 +593,19 @@ public function test_create_item_default_theme_json_version() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + wp_delete_post( $data['id'], true ); - $this->assertSame( 201, $response->get_status() ); - $this->assertArrayHasKey( 'theme_json_version', $data ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in the response data.' ); $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); - - wp_delete_post( $data['id'], true ); } /** * @dataProvider data_create_item_invalid_theme_json_version * * @covers WP_REST_Font_Faces_Controller::create_item + * + * @param int $theme_json_version Version input to test. */ public function test_create_item_invalid_theme_json_version( $theme_json_version ) { wp_set_current_user( self::$admin_id ); @@ -578,6 +617,11 @@ public function test_create_item_invalid_theme_json_version( $theme_json_version $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + /** + * Data provider. + * + * @return array + */ public function data_create_item_invalid_theme_json_version() { return array( array( 1 ), @@ -589,6 +633,8 @@ public function data_create_item_invalid_theme_json_version() { * @dataProvider data_create_item_invalid_settings * * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + * + * @param mixed $settings Settings to test. */ public function test_create_item_invalid_settings( $settings ) { wp_set_current_user( self::$admin_id ); @@ -601,6 +647,11 @@ public function test_create_item_invalid_settings( $settings ) { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + /** + * Data provider. + * + * @return array + */ public function data_create_item_invalid_settings() { return array( 'Missing fontFamily' => array( @@ -647,10 +698,10 @@ public function test_create_item_invalid_settings_json() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); $expected_message = 'font_face_settings parameter must be a valid JSON string.'; $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; - $this->assertSame( $expected_message, $message ); + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** @@ -660,47 +711,138 @@ public function test_create_item_invalid_file_src() { $files = $this->setup_font_file_upload( array( 'woff2' ) ); wp_set_current_user( self::$admin_id ); + $src = 'invalid'; $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); $request->set_param( 'theme_json_version', 2 ); $request->set_param( 'font_face_settings', wp_json_encode( - array_merge( self::$default_settings, array( 'src' => 'invalid' ) ) + array_merge( self::$default_settings, array( 'src' => $src ) ) ) ); $request->set_file_params( $files ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $expected_message = 'File ' . array_keys( $files )[0] . ' must be used in font_face_settings[src].'; + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_face_settings[src] value "' . $src . '" must be a valid URL or file reference.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_missing_file_src() { + $files = $this->setup_font_file_upload( array( 'woff2', 'woff' ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( 'src' => array( array_keys( $files )[0] ) ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'File ' . array_keys( $files )[1] . ' must be used in font_face_settings[src].'; $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; - $this->assertSame( $expected_message, $message ); + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** - * @dataProvider data_create_item_santize_font_family + * @dataProvider data_sanitize_font_face_settings * * @covers WP_REST_Font_Face_Controller::sanitize_font_face_settings + * + * @param string $settings Settings to test. + * @param string $expected Expected settings result. */ - public function test_create_item_santize_font_family( $font_family_setting, $expected ) { - $settings = array_merge( self::$default_settings, array( 'fontFamily' => $font_family_setting ) ); + public function test_create_item_sanitize_font_face_settings( $settings, $expected ) { + $settings = array_merge( self::$default_settings, $settings ); + $expected = array_merge( self::$default_settings, $expected ); wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + wp_delete_post( $data['id'], true ); - $this->assertSame( 201, $response->get_status() ); - $this->assertSame( $expected, $data['font_face_settings']['fontFamily'] ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertSame( $expected, $data['font_face_settings'], 'The response font_face_settings should match.' ); } - public function data_create_item_santize_font_family() { + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_font_face_settings() { return array( - array( 'Libre Barcode 128 Text', '"Libre Barcode 128 Text"' ), - array( 'B612 Mono', '"B612 Mono"' ), - array( 'Open Sans, Noto Sans, sans-serif', '"Open Sans", "Noto Sans", sans-serif' ), + 'settings with tags, extra whitespace, new lines' => array( + 'settings' => array( + 'fontFamily' => " Open Sans\n ", + 'fontStyle' => " oblique 20deg 50deg\n ", + 'fontWeight' => " 200\n ", + 'src' => " https://example.com/ ", + 'fontStretch' => " expanded\n ", + 'ascentOverride' => " 70%\n ", + 'descentOverride' => " 30%\n ", + 'fontVariant' => " normal\n ", + 'fontFeatureSettings' => " \"swsh\" 2\n ", + 'fontVariationSettings' => " \"xhgt\" 0.7\n ", + 'lineGapOverride' => " 10%\n ", + 'sizeAdjust' => " 90%\n ", + 'unicodeRange' => " U+0025-00FF, U+4??\n ", + 'preview' => " https://example.com/ ", + ), + 'expected' => array( + 'fontFamily' => '"Open Sans"', + 'fontStyle' => 'oblique 20deg 50deg', + 'fontWeight' => '200', + 'src' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20', + 'fontStretch' => 'expanded', + 'ascentOverride' => '70%', + 'descentOverride' => '30%', + 'fontVariant' => 'normal', + 'fontFeatureSettings' => '"swsh" 2', + 'fontVariationSettings' => '"xhgt" 0.7', + 'lineGapOverride' => '10%', + 'sizeAdjust' => '90%', + 'unicodeRange' => 'U+0025-00FF, U+4??', + 'preview' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20', + ), + ), + 'multiword font family name with integer' => array( + 'settings' => array( + 'fontFamily' => 'Libre Barcode 128 Text', + ), + 'expected' => array( + 'fontFamily' => '"Libre Barcode 128 Text"', + ), + ), + 'multiword font family name' => array( + 'settings' => array( + 'fontFamily' => 'B612 Mono', + ), + 'expected' => array( + 'fontFamily' => '"B612 Mono"', + ), + ), + 'comma-separated font family names' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans, Noto Sans, sans-serif', + ), + 'expected' => array( + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + ), + ), ); } @@ -709,6 +851,9 @@ public function data_create_item_santize_font_family() { */ // public function test_create_item_no_permission() {} + /** + * @covers WP_REST_Font_Faces_Controller::update_item + */ public function test_update_item() { $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); $response = rest_get_server()->dispatch( $request ); @@ -725,8 +870,8 @@ public function test_delete_item() { $request->set_param( 'force', true ); $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); - $this->assertNull( get_post( $font_face_id ) ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 201.' ); + $this->assertNull( get_post( $font_face_id ), 'The deleted post should not exist.' ); } /** @@ -739,15 +884,15 @@ public function test_delete_item_no_trash() { // Attempt trashing. $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'The response should return an error for "rest_trash_not_supported" with 501 status.' ); $request->set_param( 'force', 'false' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'When "force" is false, the response should return an error for "rest_trash_not_supported" with 501 status.' ); // Ensure the post still exists. $post = get_post( $font_face_id ); - $this->assertNotEmpty( $post ); + $this->assertNotEmpty( $post, 'The post should still exists.' ); } /** @@ -781,7 +926,7 @@ public function test_delete_item_invalid_parent_id() { $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$other_font_family_id . '/font-faces/' . self::$font_face_id1 ); $request->set_param( 'force', true ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_font_face_parent_id_mismatch', $response, 404 ); + $this->assertErrorResponse( 'rest_font_face_parent_id_mismatch', $response, 404, 'The response should return an error for "rest_font_face_parent_id_mismatch" with 404 status.' ); $expected_message = 'The font face does not belong to the specified font family with id of "' . self::$other_font_family_id . '"'; $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' ); @@ -796,12 +941,12 @@ public function test_delete_item_no_permissions() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_delete', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 401, 'The response should return an error for "rest_cannot_delete" with 401 status for an invalid user.' ); wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 403, 'The response should return an error for "rest_cannot_delete" with 403 status for a user without permission.' ); } /** @@ -813,7 +958,7 @@ public function test_prepare_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $this->check_font_face_data( $data, self::$font_face_id2, $response->get_links() ); } @@ -825,41 +970,79 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $properties = $data['schema']['properties']; - $this->assertCount( 4, $properties ); - $this->assertArrayHasKey( 'id', $properties ); - $this->assertArrayHasKey( 'theme_json_version', $properties ); - $this->assertArrayHasKey( 'parent', $properties ); - $this->assertArrayHasKey( 'font_face_settings', $properties ); + $this->assertCount( 4, $properties, 'There should be 4 properties in the schema::properties data.' ); + $this->assertArrayHasKey( 'id', $properties, 'The id property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'theme_json_version', $properties, 'The id property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'parent', $properties, 'The id property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'font_face_settings', $properties, 'The id property should exist in the schema::properties data.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item_schema + */ + public function test_get_item_schema_font_face_settings_should_all_have_sanitize_callbacks() { + $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_item_schema(); + $font_face_settings_schema = $schema['properties']['font_face_settings']; + + $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' ); + $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_face_settings_schema['properties'] as $property ) { + $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' ); + $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' ); + $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'The sanitize_callback value should be callable.' ); + } + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_public_item_schema + */ + public function test_get_public_item_schema_should_not_have_arg_options() { + $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_public_item_schema(); + $font_face_settings_schema = $schema['properties']['font_face_settings']; + + $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' ); + $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_face_settings_schema['properties'] as $property ) { + $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' ); + } } protected function check_font_face_data( $data, $post_id, $links ) { - $post = get_post( $post_id ); + self::$post_ids_for_cleanup[] = $post_id; + $post = get_post( $post_id ); - $this->assertArrayHasKey( 'id', $data ); - $this->assertSame( $post->ID, $data['id'] ); + $this->assertArrayHasKey( 'id', $data, 'The id property should exist in response data.' ); + $this->assertSame( $post->ID, $data['id'], 'The "id" from the response data should match the post ID.' ); - $this->assertArrayHasKey( 'parent', $data ); - $this->assertSame( $post->post_parent, $data['parent'] ); + $this->assertArrayHasKey( 'parent', $data, 'The parent property should exist in response data.' ); + $this->assertSame( $post->post_parent, $data['parent'], 'The "parent" from the response data should match the post parent.' ); - $this->assertArrayHasKey( 'theme_json_version', $data ); - $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'] ); + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in response data.' ); + $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'], 'The "theme_json_version" from the response data should match WP_Theme_JSON::LATEST_SCHEMA.' ); - $this->assertArrayHasKey( 'font_face_settings', $data ); - $this->assertSame( $post->post_content, wp_json_encode( $data['font_face_settings'] ) ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in response data.' ); + $this->assertSame( $post->post_content, wp_json_encode( $data['font_face_settings'] ), 'The encoded "font_face_settings" from the response data should match the post content.' ); - $this->assertNotEmpty( $links ); - $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), $links['self'][0]['href'] ); - $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces' ), $links['collection'][0]['href'] ); - $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->post_parent ), $links['parent'][0]['href'] ); + $this->assertNotEmpty( $links, 'The links should not be empty in the response data.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ); + $this->assertSame( $expected, $links['self'][0]['href'], 'The links URL from the response data should match the post\'s REST endpoint.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces' ); + $this->assertSame( $expected, $links['collection'][0]['href'], 'The links collection URL from the response data should match the REST endpoint.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent ); + $this->assertSame( $expected, $links['parent'][0]['href'], 'The links for a parent URL from the response data should match the parent\'s REST endpoint.' ); } - protected function check_file_meta( $font_face_id, $srcs ) { + protected function check_file_meta( $font_face_id, $src_attributes ) { $file_meta = get_post_meta( $font_face_id, '_wp_font_face_file' ); - foreach ( $srcs as $src ) { - $file_name = basename( $src ); + foreach ( $src_attributes as $src_attribute ) { + $file_name = basename( $src_attribute ); $this->assertContains( $file_name, $file_meta, 'The uploaded font file path should be saved in the post meta.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index 601ff32e2b6c9e..94ad5eccd7e570 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -7,10 +7,12 @@ * @since 6.5.0 * * @group restapi + * @group fonts + * @group font-library * - * @coversDefaultClass WP_REST_Font_Families_Controller_Test + * @coversDefaultClass WP_REST_Font_Families_Controller */ -class WP_REST_Font_Families_Controller_Test extends WP_Test_REST_Controller_Testcase { +class Tests_REST_WpRestFontFamiliesController extends WP_Test_REST_Controller_Testcase { protected static $admin_id; protected static $editor_id; @@ -20,6 +22,8 @@ class WP_REST_Font_Families_Controller_Test extends WP_Test_REST_Controller_Test protected static $font_face_id1; protected static $font_face_id2; + private static $post_ids_to_cleanup = array(); + protected static $default_settings = array( 'name' => 'Open Sans', 'slug' => 'open-sans', @@ -54,7 +58,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'fontFamily' => 'Helvetica, Arial, sans-serif', ) ); - self::$font_face_id1 = WP_REST_Font_Faces_Controller_Test::create_font_face_post( + self::$font_face_id1 = Tests_REST_WpRestFontFacesController::create_font_face_post( self::$font_family_id1, array( 'fontFamily' => '"Open Sans"', @@ -63,7 +67,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), ) ); - self::$font_face_id2 = WP_REST_Font_Faces_Controller_Test::create_font_face_post( + self::$font_face_id2 = Tests_REST_WpRestFontFacesController::create_font_face_post( self::$font_family_id1, array( 'fontFamily' => '"Open Sans"', @@ -72,16 +76,32 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ), ) ); + + static::$post_ids_to_cleanup = array(); } public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); self::delete_user( self::$editor_id ); + + wp_delete_post( self::$font_family_id1 ); + wp_delete_post( self::$font_family_id2 ); + wp_delete_post( self::$font_face_id1 ); + wp_delete_post( self::$font_face_id2 ); + } + + public function tear_down() { + foreach ( static::$post_ids_to_cleanup as $post_id ) { + wp_delete_post( $post_id, true ); + } + static::$post_ids_to_cleanup = array(); + + parent::tear_down(); } public static function create_font_family_post( $settings = array() ) { $settings = array_merge( self::$default_settings, $settings ); - return self::factory()->post->create( + $post_id = self::factory()->post->create( wp_slash( array( 'post_type' => 'wp_font_family', @@ -97,10 +117,14 @@ public static function create_font_family_post( $settings = array() ) { ) ) ); + + static::$post_ids_to_cleanup[] = $post_id; + + return $post_id; } /** - * @covers WP_REST_Font_Faces_Controller::register_routes + * @covers WP_REST_Font_Families_Controller::register_routes */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); @@ -143,29 +167,49 @@ public function test_font_families_no_autosave_routes() { } /** - * @covers WP_REST_Font_Families_Controller::get_context_param + * @doesNotPerformAssertions */ public function test_context_param() { - // Collection. - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); - $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + // See test_get_context_param(). + } - // Single. - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families/' . self::$font_family_id1 ); + /** + * @dataProvider data_get_context_param + * + * @covers WP_REST_Font_Families_Controller::get_context_param + * + * @param bool $single_route Whether to test a single route. + */ + public function test_get_context_param( $single_route ) { + $route = '/wp/v2/font-families'; + if ( $single_route ) { + $route .= '/' . self::$font_family_id1; + } + + $request = new WP_REST_Request( 'OPTIONS', $route ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); - $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + + $endpoint_data = $data['endpoints'][0]; + $this->assertArrayNotHasKey( 'allow_batch', $endpoint_data, 'The allow_batch property should not exist in the endpoint data.' ); + $this->assertSame( 'view', $endpoint_data['args']['context']['default'], 'The endpoint\'s args::context::default should be set to view.' ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $endpoint_data['args']['context']['enum'], 'The endpoint\'s args::context::enum should be set to [ view, embed, edit ].' ); } + /** + * Data provider. + * + * @return array + */ + public function data_get_context_param() { + return array( + 'Collection' => array( false ), + 'Single' => array( true ), + ); + } /** - * @covers WP_REST_Font_Faces_Controller::get_items + * @covers WP_REST_Font_Families_Controller::get_items */ public function test_get_items() { wp_set_current_user( self::$admin_id ); @@ -173,16 +217,16 @@ public function test_get_items() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertCount( 2, $data ); - $this->assertArrayHasKey( '_links', $data[0] ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertCount( 2, $data, 'There should be 2 properties in the response data.' ); + $this->assertArrayHasKey( '_links', $data[0], 'The _links property should exist in the response data 0.' ); $this->check_font_family_data( $data[0], self::$font_family_id2, $data[0]['_links'] ); - $this->assertArrayHasKey( '_links', $data[1] ); + $this->assertArrayHasKey( '_links', $data[1], 'The _links property should exist in the response data 1.' ); $this->check_font_family_data( $data[1], self::$font_family_id1, $data[1]['_links'] ); } /** - * @covers WP_REST_Font_Faces_Controller::get_items + * @covers WP_REST_Font_Families_Controller::get_items */ public function test_get_items_by_slug() { $font_family = get_post( self::$font_family_id2 ); @@ -193,28 +237,29 @@ public function test_get_items_by_slug() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertCount( 1, $data ); - $this->assertSame( $font_family->ID, $data[0]['id'] ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertCount( 1, $data, 'There should be 2 properties in the response data.' ); + $this->assertArrayHasKey( 'id', $data[0], 'The id property should exist in the response data.' ); + $this->assertSame( $font_family->ID, $data[0]['id'], 'The id should match the expected ID in the response data.' ); } /** - * @covers WP_REST_Font_Faces_Controller::get_items + * @covers WP_REST_Font_Families_Controller::get_items */ public function test_get_items_no_permission() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item() { wp_set_current_user( self::$admin_id ); @@ -222,12 +267,12 @@ public function test_get_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $this->check_font_family_data( $data, self::$font_family_id1, $response->get_links() ); } /** - * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response */ public function test_get_item_embedded_font_faces() { wp_set_current_user( self::$admin_id ); @@ -236,19 +281,19 @@ public function test_get_item_embedded_font_faces() { $response = rest_get_server()->dispatch( $request ); $data = rest_get_server()->response_to_data( $response, true ); - $this->assertSame( 200, $response->get_status() ); - $this->assertArrayHasKey( '_embedded', $data ); - $this->assertArrayHasKey( 'font_faces', $data['_embedded'] ); - $this->assertCount( 2, $data['_embedded']['font_faces'] ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayHasKey( '_embedded', $data, 'The _embedded property should exist in the response data.' ); + $this->assertArrayHasKey( 'font_faces', $data['_embedded'], 'The font_faces property should exist in _embedded data.' ); + $this->assertCount( 2, $data['_embedded']['font_faces'], 'There should be 2 font_faces in the _embedded data.' ); foreach ( $data['_embedded']['font_faces'] as $font_face ) { - $this->assertArrayHasKey( 'id', $font_face ); + $this->assertArrayHasKey( 'id', $font_face, 'The id property should exist in the _embedded font_face data.' ); $font_face_request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 . '/font-faces/' . $font_face['id'] ); $font_face_response = rest_get_server()->dispatch( $font_face_request ); $font_face_data = rest_get_server()->response_to_data( $font_face_response, true ); - $this->assertSame( $font_face_data, $font_face ); + $this->assertSame( $font_face_data, $font_face, 'The embedded font_face data should match when the data from a single request.' ); } } @@ -263,10 +308,8 @@ public function test_get_item_removes_extra_settings() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'] ); - - wp_delete_post( $font_family_id, true ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'], 'The fontFace property should not exist in the font_family_settings data.' ); } /** @@ -281,6 +324,8 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { ) ); + static::$post_ids_to_cleanup[] = $font_family_id; + $empty_settings = array( 'name' => '', // Slug will default to the post id. @@ -294,14 +339,12 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertSame( $empty_settings, $data['font_family_settings'] ); - - wp_delete_post( $font_family_id, true ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( $empty_settings, $data['font_family_settings'], 'The empty settings should exist in the font_family_settings data.' ); } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item_invalid_font_family_id() { wp_set_current_user( self::$admin_id ); @@ -311,22 +354,22 @@ public function test_get_item_invalid_font_family_id() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item_no_permission() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); wp_set_current_user( self::$editor_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); } /** - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Families_Controller::create_item */ public function test_create_item() { $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); @@ -338,18 +381,16 @@ public function test_create_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 201, $response->get_status() ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); $this->check_font_family_data( $data, $data['id'], $response->get_links() ); $reponse_settings = $data['font_family_settings']; - $this->assertSame( $settings, $reponse_settings ); - $this->assertEmpty( $data['font_faces'] ); - - wp_delete_post( $data['id'], true ); + $this->assertSame( $settings, $reponse_settings, 'The expected settings should exist in the font_family_settings data.' ); + $this->assertEmpty( $data['font_faces'], 'The font_faces should be empty or not exist in the response data.' ); } /** - * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_request + * @covers WP_REST_Font_Families_Controller::validate_create_font_face_request */ public function test_create_item_default_theme_json_version() { $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); @@ -360,17 +401,19 @@ public function test_create_item_default_theme_json_version() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 201, $response->get_status() ); - $this->assertArrayHasKey( 'theme_json_version', $data ); - $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); + static::$post_ids_to_cleanup[] = $data['id']; - wp_delete_post( $data['id'], true ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in the response data.' ); + $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); } /** * @dataProvider data_create_item_invalid_theme_json_version * - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Families_Controller::create_item + * + * @param int $theme_json_version Version to test. */ public function test_create_item_invalid_theme_json_version( $theme_json_version ) { wp_set_current_user( self::$admin_id ); @@ -382,6 +425,11 @@ public function test_create_item_invalid_theme_json_version( $theme_json_version $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + /** + * Data provider. + * + * @return array + */ public function data_create_item_invalid_theme_json_version() { return array( array( 1 ), @@ -392,7 +440,9 @@ public function data_create_item_invalid_theme_json_version() { /** * @dataProvider data_create_item_with_default_preview * - * @covers WP_REST_Font_Faces_Controller::sanitize_font_family_settings + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param array $settings Settings to test. */ public function test_create_item_with_default_preview( $settings ) { wp_set_current_user( self::$admin_id ); @@ -402,14 +452,19 @@ public function test_create_item_with_default_preview( $settings ) { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 201, $response->get_status() ); - $response_settings = $data['font_family_settings']; - $this->assertArrayHasKey( 'preview', $response_settings ); - $this->assertSame( '', $response_settings['preview'] ); + static::$post_ids_to_cleanup[] = $data['id']; - wp_delete_post( $data['id'], true ); + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $response_settings = $data['font_family_settings']; + $this->assertArrayHasKey( 'preview', $response_settings, 'The preview property should exist in the font_family_settings data.' ); + $this->assertSame( '', $response_settings['preview'], 'The preview data should be an empty string.' ); } + /** + * Data provider. + * + * @return array + */ public function data_create_item_with_default_preview() { $default_settings = array( 'name' => 'Open Sans', @@ -426,10 +481,90 @@ public function data_create_item_with_default_preview() { ); } + /** + * @dataProvider data_sanitize_font_family_settings + * + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param string $settings Font family settings to test. + * @param string $expected Expected settings result. + */ + public function test_create_item_santize_font_family_settings( $settings, $expected ) { + $settings = array_merge( self::$default_settings, $settings ); + $expected = array_merge( self::$default_settings, $expected ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + static::$post_ids_to_cleanup[] = $data['id']; + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_font_family_settings() { + return array( + 'settings with tags, extra whitespace, new lines' => array( + 'settings' => array( + 'name' => " Opening Sans\n ", + 'slug' => " OPENing SanS \n ", + 'fontFamily' => " Opening Sans\n ", + 'preview' => " https://example.com/ ", + ), + 'expected' => array( + 'name' => 'Opening Sans', + 'slug' => 'opening-sans-alertxss', + 'fontFamily' => '"Opening Sans"', + 'preview' => "https://example.com//stylescriptalert('XSS');/script%20%20%20%20%20%20", + ), + ), + 'multiword font family name with integer' => array( + 'settings' => array( + 'slug' => 'libre-barcode-128-text', + 'fontFamily' => 'Libre Barcode 128 Text', + ), + 'expected' => array( + 'slug' => 'libre-barcode-128-text', + 'fontFamily' => '"Libre Barcode 128 Text"', + ), + ), + 'multiword font family name' => array( + 'settings' => array( + 'slug' => 'b612-mono', + 'fontFamily' => 'B612 Mono', + ), + 'expected' => array( + 'slug' => 'b612-mono', + 'fontFamily' => '"B612 Mono"', + ), + ), + 'comma-separated font family names' => array( + 'settings' => array( + 'slug' => 'open-sans-noto-sans', + 'fontFamily' => 'Open Sans, Noto Sans, sans-serif', + ), + 'expected' => array( + 'slug' => 'open-sans-noto-sans', + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + ), + ), + ); + } + /** * @dataProvider data_create_item_invalid_settings * - * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + * @covers WP_REST_Font_Families_Controller::validate_create_font_face_settings + * + * @param array $settings Settings to test. */ public function test_create_item_invalid_settings( $settings ) { wp_set_current_user( self::$admin_id ); @@ -441,6 +576,11 @@ public function test_create_item_invalid_settings( $settings ) { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + /** + * Data provider. + * + * @return array + */ public function data_create_item_invalid_settings() { return array( 'Missing name' => array( @@ -484,10 +624,10 @@ public function test_create_item_invalid_settings_json() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); $expected_message = 'font_family_settings parameter must be a valid JSON string.'; $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; - $this->assertSame( $expected_message, $message ); + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** @@ -501,14 +641,14 @@ public function test_create_item_with_duplicate_slug() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_duplicate_font_family', $response, 400 ); + $this->assertErrorResponse( 'rest_duplicate_font_family', $response, 400, 'The response should return an error for "rest_duplicate_font_family" with 400 status.' ); $expected_message = 'A font family with slug "helvetica" already exists.'; $message = $response->as_error()->get_error_messages()[0]; - $this->assertSame( $expected_message, $message ); + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Families_Controller::create_item */ public function test_create_item_no_permission() { $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); @@ -516,7 +656,7 @@ public function test_create_item_no_permission() { $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_create', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_create', $response, 401, 'The response should return an error for "rest_cannot_create" with 401 status.' ); wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); @@ -532,7 +672,7 @@ public function test_create_item_no_permission() { ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_create', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_create', $response, 403, 'The response should return an error for "rest_cannot_create" with 403 status.' ); } /** @@ -556,7 +696,7 @@ public function test_update_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $this->check_font_family_data( $data, $font_family_id, $response->get_links() ); $expected_settings = array( @@ -565,14 +705,15 @@ public function test_update_item() { 'fontFamily' => $settings['fontFamily'], 'preview' => $settings['preview'], ); - $this->assertSame( $expected_settings, $data['font_family_settings'] ); - - wp_delete_post( $font_family_id, true ); + $this->assertSame( $expected_settings, $data['font_family_settings'], 'The response font_family_settings should match expected settings.' ); } /** * @dataProvider data_update_item_individual_settings + * * @covers WP_REST_Font_Families_Controller::update_item + * + * @param array $settings Settings to test. */ public function test_update_item_individual_settings( $settings ) { wp_set_current_user( self::$admin_id ); @@ -583,15 +724,18 @@ public function test_update_item_individual_settings( $settings ) { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $key = key( $settings ); $value = current( $settings ); - $this->assertArrayHasKey( $key, $data['font_family_settings'] ); - $this->assertSame( $value, $data['font_family_settings'][ $key ] ); - - wp_delete_post( $font_family_id, true ); + $this->assertArrayHasKey( $key, $data['font_family_settings'], 'The expected key should exist in the font_family_settings data.' ); + $this->assertSame( $value, $data['font_family_settings'][ $key ], 'The font_family_settings data should match.' ); } + /** + * Data provider. + * + * @return array + */ public function data_update_item_individual_settings() { return array( array( array( 'name' => 'Opened Sans' ) ), @@ -602,38 +746,39 @@ public function data_update_item_individual_settings() { ); } - /** - * @dataProvider data_update_item_santize_font_family + /** + * @dataProvider data_sanitize_font_family_settings * - * @covers WP_REST_Font_Families_Controller::sanitize_font_face_settings + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param string $settings Font family settings to test. + * @param string $expected Expected settings result. */ - public function test_update_item_santize_font_family( $font_family_setting, $expected ) { + public function test_update_item_santize_font_family_settings( $settings, $expected ) { + // Unset/modify slug from the data provider, since we're updating rather than creating. + unset( $settings['slug'] ); + $initial_settings = array( 'slug' => 'open-sans-update' ); + $expected = array_merge( self::$default_settings, $expected, $initial_settings ); + wp_set_current_user( self::$admin_id ); + $font_family_id = self::create_font_family_post( $initial_settings ); + static::$post_ids_to_cleanup[] = $font_family_id; - $font_family_id = self::create_font_family_post(); - $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); - $request->set_param( 'font_family_settings', wp_json_encode( array( 'fontFamily' => $font_family_setting ) ) ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertSame( $expected, $data['font_family_settings']['fontFamily'] ); - - wp_delete_post( $font_family_id, true ); - } - - public function data_update_item_santize_font_family() { - return array( - array( 'Libre Barcode 128 Text', '"Libre Barcode 128 Text"' ), - array( 'B612 Mono', '"B612 Mono"' ), - array( 'Open Sans, Noto Sans, sans-serif', '"Open Sans", "Noto Sans", sans-serif' ), - ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' ); } /** * @dataProvider data_update_item_invalid_settings * - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item + * + * @param array $settings Settings to test. */ public function test_update_item_empty_settings( $settings ) { wp_set_current_user( self::$admin_id ); @@ -646,6 +791,11 @@ public function test_update_item_empty_settings( $settings ) { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + /** + * Data provider. + * + * @return array + */ public function data_update_item_invalid_settings() { return array( 'Empty name' => array( @@ -664,7 +814,7 @@ public function data_update_item_invalid_settings() { } /** - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_update_slug_not_allowed() { wp_set_current_user( self::$admin_id ); @@ -675,14 +825,14 @@ public function test_update_item_update_slug_not_allowed() { ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); $expected_message = 'font_family_settings[slug] cannot be updated.'; $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; - $this->assertSame( $expected_message, $message ); + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_invalid_font_family_id() { $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) ); @@ -691,11 +841,11 @@ public function test_update_item_invalid_font_family_id() { $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404, 'The response should return an error for "rest_post_invalid_id" with 404 status.' ); } /** - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_no_permission() { $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) ); @@ -704,18 +854,18 @@ public function test_update_item_no_permission() { $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401, 'The response should return an error for "rest_cannot_edit" with 401 status for an invalid user.' ); wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403, 'The response should return an error for "rest_cannot_edit" with 403 status for a user without permission.' ); } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item() { wp_set_current_user( self::$admin_id ); @@ -724,12 +874,12 @@ public function test_delete_item() { $request['force'] = true; $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); - $this->assertNull( get_post( $font_family_id ) ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertNull( get_post( $font_family_id ), 'The post should not exist after deleting.' ); } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item_no_trash() { wp_set_current_user( self::$admin_id ); @@ -738,21 +888,19 @@ public function test_delete_item_no_trash() { // Attempt trashing. $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'The response should return an error for "rest_trash_not_supported" with 501 status.' ); $request->set_param( 'force', 'false' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'When "force" is false, the response should return an error for "rest_trash_not_supported" with 501 status.' ); // Ensure the post still exists. $post = get_post( $font_family_id ); - $this->assertNotEmpty( $post ); - - wp_delete_post( $font_family_id, true ); + $this->assertNotEmpty( $post, 'The post should still exist.' ); } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item_invalid_font_family_id() { wp_set_current_user( self::$admin_id ); @@ -762,7 +910,7 @@ public function test_delete_item_invalid_font_family_id() { } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item_no_permissions() { $font_family_id = self::create_font_family_post(); @@ -770,18 +918,16 @@ public function test_delete_item_no_permissions() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_delete', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 401, 'The response should return an error for "rest_cannot_delete" with 401 status for an invalid user.' ); wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 ); - - wp_delete_post( $font_family_id, true ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 403, 'The response should return an error for "rest_cannot_delete" with 403 status for a user without permission.' ); } /** - * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response */ public function test_prepare_item() { wp_set_current_user( self::$admin_id ); @@ -789,35 +935,70 @@ public function test_prepare_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $this->check_font_family_data( $data, self::$font_family_id2, $response->get_links() ); } /** - * @covers WP_REST_Font_Faces_Controller::get_item_schema + * @covers WP_REST_Font_Families_Controller::get_item_schema */ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); $properties = $data['schema']['properties']; - $this->assertCount( 4, $properties ); - $this->assertArrayHasKey( 'id', $properties ); - $this->assertArrayHasKey( 'theme_json_version', $properties ); - $this->assertArrayHasKey( 'font_faces', $properties ); - $this->assertArrayHasKey( 'font_family_settings', $properties ); + $this->assertCount( 4, $properties, 'There should be 4 properties in the schema::properties data.' ); + $this->assertArrayHasKey( 'id', $properties, 'The id property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'theme_json_version', $properties, 'The theme_json_version property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'font_faces', $properties, 'The font_faces property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'font_family_settings', $properties, 'The font_family_settings property should exist in the schema::properties data.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item_schema + */ + public function test_get_item_schema_font_family_settings_should_all_have_sanitize_callbacks() { + $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_item_schema(); + $font_family_settings_schema = $schema['properties']['font_family_settings']; + + $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' ); + $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_family_settings_schema['properties'] as $property ) { + $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' ); + $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' ); + $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'That sanitize_callback value should be callable.' ); + } + } + + /** + * @covers WP_REST_Font_Families_Controller::get_public_item_schema + */ + public function test_get_public_item_schema_should_not_have_arg_options() { + $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_public_item_schema(); + $font_family_settings_schema = $schema['properties']['font_family_settings']; + + $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' ); + $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_family_settings_schema['properties'] as $property ) { + $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' ); + } } protected function check_font_family_data( $data, $post_id, $links ) { - $post = get_post( $post_id ); + static::$post_ids_to_cleanup[] = $post_id; + $post = get_post( $post_id ); - $this->assertArrayHasKey( 'id', $data ); - $this->assertSame( $post->ID, $data['id'] ); + $this->assertArrayHasKey( 'id', $data, 'The id property should exist in response data.' ); + $this->assertSame( $post->ID, $data['id'], 'The "id" from the response data should match the post ID.' ); - $this->assertArrayHasKey( 'theme_json_version', $data ); - $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'] ); + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in response data.' ); + $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, $data['theme_json_version'], 'The "theme_json_version" from the response data should match WP_Theme_JSON::LATEST_SCHEMA.' ); $font_face_ids = get_children( array( @@ -828,13 +1009,13 @@ protected function check_font_family_data( $data, $post_id, $links ) { 'orderby' => 'ID', ) ); - $this->assertArrayHasKey( 'font_faces', $data ); + $this->assertArrayHasKey( 'font_faces', $data, 'The font_faces property should exist in the response data.' ); foreach ( $font_face_ids as $font_face_id ) { - $this->assertContains( $font_face_id, $data['font_faces'] ); + $this->assertContains( $font_face_id, $data['font_faces'], 'The ID is in the font_faces data.' ); } - $this->assertArrayHasKey( 'font_family_settings', $data ); + $this->assertArrayHasKey( 'font_family_settings', $data, 'The font_family_settings property should exist in the response data.' ); $settings = $data['font_family_settings']; $expected_settings = array( 'name' => $post->post_title, @@ -842,11 +1023,13 @@ protected function check_font_family_data( $data, $post_id, $links ) { 'fontFamily' => $settings['fontFamily'], 'preview' => $settings['preview'], ); - $this->assertSame( $expected_settings, $settings ); + $this->assertSame( $expected_settings, $settings, 'The font_family_settings should match.' ); - $this->assertNotEmpty( $links ); - $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->ID ), $links['self'][0]['href'] ); - $this->assertSame( rest_url( 'wp/v2/font-families' ), $links['collection'][0]['href'] ); + $this->assertNotEmpty( $links, 'The links should not be empty in the response data.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->ID ); + $this->assertSame( $expected, $links['self'][0]['href'], 'The links URL from the response data should match the post\'s REST endpoint.' ); + $expected = rest_url( 'wp/v2/font-families' ); + $this->assertSame( $expected, $links['collection'][0]['href'], 'The links collection URL from the response data should match the REST endpoint.' ); if ( ! $font_face_ids ) { return; @@ -855,12 +1038,13 @@ protected function check_font_family_data( $data, $post_id, $links ) { // Check font_face links, if present. $this->assertArrayHasKey( 'font_faces', $links ); foreach ( $links['font_faces'] as $index => $link ) { - $this->assertSame( rest_url( 'wp/v2/font-families/' . $post->ID . '/font-faces/' . $font_face_ids[ $index ] ), $link['href'] ); + $expected = rest_url( 'wp/v2/font-families/' . $post->ID . '/font-faces/' . $font_face_ids[ $index ] ); + $this->assertSame( $expected, $link['href'], 'The links for a font faces URL from the response data should match the REST endpoint.' ); $embeddable = isset( $link['attributes']['embeddable'] ) ? $link['attributes']['embeddable'] : $link['embeddable']; - $this->assertTrue( $embeddable ); + $this->assertTrue( $embeddable, 'The embeddable should be true.' ); } } }