diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 7ebc3a1d3bfe1..7eff4a31e6c55 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -749,4 +749,8 @@ // Font management. add_action( 'wp_head', 'wp_print_font_faces', 50 ); +// Font library filters. +add_action( 'deleted_post', '_wp_after_delete_font_family', 10, 2 ); +add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 ); + unset( $filter, $action ); diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php new file mode 100644 index 0000000000000..4ed5b547e458f --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -0,0 +1,260 @@ +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.' ), $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.' ); + _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.' ) ); + } + + 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".' ), $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.' ) ); + } + + // 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."' ), + $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/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php new file mode 100644 index 0000000000000..3e48a1d797b6b --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -0,0 +1,155 @@ += 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 string $slug Font collection slug. + * @param array $data_or_file Font collection data array or a path/URL to a JSON file + * containing the font collection. + * See {@see wp_register_font_collection()} for the supported fields. + * @return WP_Font_Collection|WP_Error A font collection if it was registered successfully, + * or WP_Error object on failure. + */ + public static function register_font_collection( $slug, $data_or_file ) { + $new_collection = new WP_Font_Collection( $slug, $data_or_file ); + + if ( self::is_collection_registered( $new_collection->slug ) ) { + $error_message = sprintf( + /* translators: %s: Font collection slug. */ + __( 'Font collection with slug: "%s" is already registered.' ), + $new_collection->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 $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.' ), $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 WP_Font_Collection|WP_Error Font collection object, + * or WP_Error object if the font collection doesn't exist. + */ + 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/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php new file mode 100644 index 0000000000000..5c1f2bc2dd73f --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -0,0 +1,214 @@ + '', + '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 ); + } +} diff --git a/src/wp-includes/fonts/fonts.php b/src/wp-includes/fonts/fonts.php new file mode 100644 index 0000000000000..81126ef9dc739 --- /dev/null +++ b/src/wp-includes/fonts/fonts.php @@ -0,0 +1,145 @@ +post_type ) { + return; + } + + $font_faces = get_children( + array( + 'post_parent' => $post_id, + 'post_type' => 'wp_font_face', + ) + ); + + foreach ( $font_faces as $font_face ) { + wp_delete_post( $font_face->ID, true ); + } +} + +/** + * Deletes associated font files when a font face is deleted. + * + * @access private + * @since 6.5.0 + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ +function _wp_before_delete_font_face( $post_id, $post ) { + if ( 'wp_font_face' !== $post->post_type ) { + return; + } + + $font_files = get_post_meta( $post_id, '_wp_font_face_file', false ); + + foreach ( $font_files as $font_file ) { + wp_delete_file( wp_get_font_dir()['path'] . '/' . $font_file ); + } +} \ No newline at end of file diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 001ccaf7d0465..fc9bd89147a57 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -564,6 +564,62 @@ function create_initial_post_types() { ) ); + register_post_type( + 'wp_font_family', + array( + 'labels' => array( + 'name' => __( 'Font Families' ), + 'singular_name' => __( 'Font Family' ), + ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => true, + 'query_var' => false, + 'show_in_rest' => false, + ) + ); + + register_post_type( + 'wp_font_face', + array( + 'labels' => array( + 'name' => __( 'Font Faces' ), + 'singular_name' => __( 'Font Face' ), + ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => true, + 'query_var' => false, + 'show_in_rest' => false, + ) + ); + register_post_status( 'publish', array( diff --git a/src/wp-settings.php b/src/wp-settings.php index a80c661d52a65..22683b37d1f5d 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -374,7 +374,10 @@ require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-css-rules-store.php'; require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-processor.php'; require ABSPATH . WPINC . '/fonts/class-wp-font-face-resolver.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-collection.php'; require ABSPATH . WPINC . '/fonts/class-wp-font-face.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-library.php'; +require ABSPATH . WPINC . '/fonts/class-wp-font-utils.php'; require ABSPATH . WPINC . '/fonts.php'; require ABSPATH . WPINC . '/class-wp-script-modules.php'; require ABSPATH . WPINC . '/script-modules.php'; diff --git a/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php new file mode 100644 index 0000000000000..550f3992c9f73 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php @@ -0,0 +1,88 @@ +post->create( + array( + 'post_type' => 'wp_font_family', + ) + ); + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $font_family_id, + ) + ); + $other_font_family_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_family', + ) + ); + $other_font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $other_font_family_id, + ) + ); + + wp_delete_post( $font_family_id, true ); + + $this->assertNull( get_post( $font_face_id ), '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() { + list( $font_face_id, $font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.woff2' ); + list( , $other_font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.ttf' ); + + wp_delete_post( $font_face_id, true ); + + $this->assertFalse( file_exists( $font_path ), '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 ) { + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + ) + ); + + $font_file = $this->upload_font_file( $filename ); + + // Make sure the font file uploaded successfully. + $this->assertFalse( $font_file['error'] ); + + $font_path = $font_file['file']; + $font_filename = basename( $font_path ); + add_post_meta( $font_face_id, '_wp_font_face_file', $font_filename ); + + return array( $font_face_id, $font_path ); + } + + protected function upload_font_file( $font_filename ) { + // @core-merge Use `DIR_TESTDATA` instead of `GUTENBERG_DIR_TESTDATA`. + $font_file_path = GUTENBERG_DIR_TESTDATA . 'fonts/' . $font_filename; + + add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_dir', 'wp_get_font_dir' ); + $font_file = wp_upload_bits( + $font_filename, + null, + file_get_contents( $font_file_path ) + ); + remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + + return $font_file; + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php new file mode 100644 index 0000000000000..a0693ce341456 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -0,0 +1,26 @@ +setExpectedIncorrectUsage( 'WP_Font_Collection::__construct' ); + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock ' ), + ); + + $collection = new WP_Font_Collection( 'slug with spaces', $mock_collection_data ); + + $this->assertSame( 'slug-with-spaces', $collection->slug, 'Slug is not sanitized.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php new file mode 100644 index 0000000000000..1cb16e271db61 --- /dev/null +++ b/tests/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/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php new file mode 100644 index 0000000000000..e8d970f5b3d39 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -0,0 +1,26 @@ +getProperty( 'collections' ); + $property->setAccessible( true ); + $property->setValue( array() ); + } + + public function set_up() { + parent::set_up(); + $this->reset_font_collections(); + } + + public function tear_down() { + parent::tear_down(); + $this->reset_font_collections(); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php new file mode 100644 index 0000000000000..2c246de80b8b9 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -0,0 +1,30 @@ + 'Test Collection', + 'font_families' => array( 'mock' ), + ); + + wp_register_font_collection( 'my-font-collection', $mock_collection_data ); + $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' ); + $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); + } + + public function test_should_get_no_font_collection_if_the_slug_is_not_registered() { + $font_collection = WP_Font_Library::get_font_collection( 'not-registered-font-collection' ); + $this->assertWPError( $font_collection ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php new file mode 100644 index 0000000000000..394c977799a06 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -0,0 +1,34 @@ +assertEmpty( $font_collections, 'Should return an empty array.' ); + } + + public function test_should_get_mock_font_collection() { + $my_font_collection_config = array( + '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', $my_font_collection_config ); + + $font_collections = WP_Font_Library::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/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php new file mode 100644 index 0000000000000..4fb97e754611f --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -0,0 +1,89 @@ +assertSame( $expected, $mimes ); + } + + /** + * 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/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php new file mode 100644 index 0000000000000..24529d0dd090b --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -0,0 +1,40 @@ + 'My Collection', + 'font_families' => array( 'mock' ), + ); + + $collection = WP_Font_Library::register_font_collection( 'my-collection', $config ); + $this->assertInstanceOf( 'WP_Font_Collection', $collection ); + } + + public function test_should_return_error_if_slug_is_repeated() { + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), + ); + + // Register first collection. + $collection1 = WP_Font_Library::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. + WP_Font_Library::register_font_collection( 'my-collection-1', $mock_collection_data ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php new file mode 100644 index 0000000000000..657d3bb07aaaf --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -0,0 +1,46 @@ + 'Test Collection', + 'font_families' => array( 'mock' ), + ); + + // Registers two mock font collections. + WP_Font_Library::register_font_collection( 'mock-font-collection-1', $mock_collection_data ); + WP_Font_Library::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(); + $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(); + $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' ); + + // Checks that all font collections were unregistered. + $this->assertEmpty( $collections, 'Font collections were not unregistered.' ); + } + + 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, 'No collections should be registered.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php new file mode 100644 index 0000000000000..de0b02e63185e --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php @@ -0,0 +1,92 @@ +assertSame( $expected_slug, $slug ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_font_face_slug_normalizes_values() { + return array( + 'Sets defaults' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Converts normal weight to 400' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => 'normal', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Converts bold weight to 700' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => 'bold', + ), + 'expected_slug' => 'open sans;normal;700;100%;U+0-10FFFF', + ), + 'Converts normal font-stretch to 100%' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontStretch' => 'normal', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes double quotes from fontFamilies' => array( + 'settings' => array( + 'fontFamily' => '"Open Sans"', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes single quotes from fontFamilies' => array( + 'settings' => array( + 'fontFamily' => "'Open Sans'", + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes spaces between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans, serif', + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + 'Removes tabs between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => "Open Sans,\tserif", + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + 'Removes new lines between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => "Open Sans,\nserif", + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php new file mode 100644 index 0000000000000..71511331c65dc --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php @@ -0,0 +1,63 @@ +assertSame( + $expected, + WP_Font_Utils::sanitize_font_family( + $font_family + ) + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_sanitize_font_family() { + return array( + 'data_families_with_spaces_and_numbers' => array( + 'font_family' => 'Rock 3D , Open Sans,serif', + 'expected' => '"Rock 3D", "Open Sans", serif', + ), + 'data_single_font_family' => array( + 'font_family' => 'Rock 3D', + 'expected' => '"Rock 3D"', + ), + 'data_no_spaces' => array( + 'font_family' => 'Rock3D', + 'expected' => 'Rock3D', + ), + 'data_many_spaces_and_existing_quotes' => array( + 'font_family' => 'Rock 3D serif, serif,sans-serif, "Open Sans"', + 'expected' => '"Rock 3D serif", serif, sans-serif, "Open Sans"', + ), + 'data_empty_family' => array( + 'font_family' => ' ', + 'expected' => '', + ), + 'data_font_family_with_whitespace_tags_new_lines' => array( + 'font_family' => " Rock 3D\n ", + 'expected' => '"Rock 3D"', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php new file mode 100644 index 0000000000000..88983fe15a14e --- /dev/null +++ b/tests/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/tests/phpunit/tests/fonts/font-library/wpFontsDir.php b/tests/phpunit/tests/fonts/font-library/wpFontsDir.php new file mode 100644 index 0000000000000..a8f79888315bd --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontsDir.php @@ -0,0 +1,72 @@ + path_join( WP_CONTENT_DIR, 'fonts' ), + 'url' => content_url( 'fonts' ), + 'subdir' => '', + 'basedir' => path_join( WP_CONTENT_DIR, 'fonts' ), + 'baseurl' => content_url( 'fonts' ), + 'error' => false, + ); + } + + public function test_fonts_dir() { + $font_dir = wp_get_font_dir(); + + $this->assertSame( $font_dir, static::$dir_defaults ); + } + + public function test_fonts_dir_with_filter() { + // Define a callback function to pass to the filter. + function set_new_values( $defaults ) { + $defaults['path'] = '/custom-path/fonts/my-custom-subdir'; + $defaults['url'] = 'http://example.com/custom-path/fonts/my-custom-subdir'; + $defaults['subdir'] = 'my-custom-subdir'; + $defaults['basedir'] = '/custom-path/fonts'; + $defaults['baseurl'] = 'http://example.com/custom-path/fonts'; + $defaults['error'] = false; + return $defaults; + } + + // Add the filter. + add_filter( 'font_dir', 'set_new_values' ); + + // Gets the fonts dir. + $font_dir = wp_get_font_dir(); + + $expected = array( + 'path' => '/custom-path/fonts/my-custom-subdir', + 'url' => 'http://example.com/custom-path/fonts/my-custom-subdir', + 'subdir' => 'my-custom-subdir', + 'basedir' => '/custom-path/fonts', + 'baseurl' => 'http://example.com/custom-path/fonts', + 'error' => false, + ); + + // 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->assertSame( static::$dir_defaults, $font_dir, 'The wp_get_font_dir() method should return the default values.' ); + } +}