diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 7ebc3a1d3bfe1..19a9285ed2fec 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -748,5 +748,7 @@ // Font management. add_action( 'wp_head', 'wp_print_font_faces', 50 ); +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.php b/src/wp-includes/fonts.php index 87503c275f390..dae5283d65ce5 100644 --- a/src/wp-includes/fonts.php +++ b/src/wp-includes/fonts.php @@ -51,3 +51,140 @@ function wp_print_font_faces( $fonts = array() ) { $wp_font_face = new WP_Font_Face(); $wp_font_face->generate_and_print( $fonts ); } + +/** + * Registers a new Font Collection in the Font Library. + * + * @since 6.5.0 + * + * @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. + * + * @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 if it was registered + * successfully, or WP_Error object on failure. + */ +function wp_register_font_collection( $slug, $data_or_file ) { + return WP_Font_Library::get_instance()->register_font_collection( $slug, $data_or_file ); +} + +/** + * Unregisters a font collection from the Font Library. + * + * @since 6.5.0 + * + * @param string $slug Font collection slug. + * @return bool True if the font collection was unregistered successfully, else false. + */ +function wp_unregister_font_collection( $slug ) { + return WP_Font_Library::get_instance()->unregister_font_collection( $slug ); +} + +/** + * Returns an array containing the current fonts upload directory's path and URL. + * + * @since 6.5.0 + * + * @param array $defaults { + * Array of information about the upload directory. + * + * @type string $path Base directory and subdirectory or full path to the fonts upload directory. + * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. + * @type string $subdir Subdirectory + * @type string $basedir Path without subdir. + * @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. + * + * @type string $path Base directory and subdirectory or full path to the fonts upload directory. + * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. + * @type string $subdir Subdirectory + * @type string $basedir Path without subdir. + * @type string $baseurl URL path without subdir. + * @type string|false $error False or error message. + * } + */ +function wp_get_font_dir( $defaults = array() ) { + $site_path = ''; + if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { + $site_path = '/sites/' . get_current_blog_id(); + } + + // Sets the defaults. + $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; + $defaults['subdir'] = ''; + $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; + $defaults['error'] = false; + + /** + * 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 ); +} + +/** + * Deletes child font faces when a font family is deleted. + * + * @access private + * @since 6.5.0 + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ +function _wp_after_delete_font_family( $post_id, $post ) { + if ( 'wp_font_family' !== $post->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 ); + $font_dir = wp_get_font_dir()['path']; + + foreach ( $font_files as $font_file ) { + wp_delete_file( $font_dir . '/' . $font_file ); + } +} 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..240bba35e94f8 --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-collection.php @@ -0,0 +1,260 @@ +slug = sanitize_title( $slug ); + 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' + ); + } + + 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; + } + } + + /** + * 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 HTTP 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, e.g. "font_families". + __( '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' => static 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..a0c07eeffcd8f --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-library.php @@ -0,0 +1,144 @@ +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 ); + } + $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 ( $this->is_collection_registered( $slug ) ) { + 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/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..8efa7978a8256 --- /dev/null +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -0,0 +1,238 @@ + '', + '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 ); + } + + /** + * Returns the expected mime-type values for font files, depending on PHP version. + * + * This is needed because font mime types vary by PHP version, so checking the PHP version + * is necessary until a list of valid mime-types for each file extension can be provided to + * the 'upload_mimes' filter. + * + * @since 6.5.0 + * + * @access private + * + * @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/src/wp-includes/post.php b/src/wp-includes/post.php index 001ccaf7d0465..eb0d8c1926190 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -564,6 +564,64 @@ 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, + 'rewrite' => 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, + 'rewrite' => 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/data/fonts/OpenSans-Regular.otf b/tests/phpunit/data/fonts/OpenSans-Regular.otf new file mode 100644 index 0000000000000..8db0f64c67ddd Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.otf differ diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.ttf b/tests/phpunit/data/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000..ae716936e9e4c Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.ttf differ diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.woff b/tests/phpunit/data/fonts/OpenSans-Regular.woff new file mode 100644 index 0000000000000..bd0f824b207d6 Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.woff differ diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.woff2 b/tests/phpunit/data/fonts/OpenSans-Regular.woff2 new file mode 100644 index 0000000000000..f778f9c8455f2 Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.woff2 differ 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..083c12202aa34 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php @@ -0,0 +1,87 @@ +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 ) { + $font_file_path = DIR_TESTDATA . '/fonts/' . $font_filename; + + 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, + null, + file_get_contents( $font_file_path ) + ); + remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_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..8a0af0c97d967 --- /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://example.com/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://example.com/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://example.com/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://example.com/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://example.com/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://example.com/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..135329e5add73 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -0,0 +1,25 @@ +get_font_collections(); + foreach ( $collections as $slug => $collection ) { + WP_Font_Library::get_instance()->unregister_font_collection( $slug ); + } + } + + 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..675efe81aec59 --- /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_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_instance()->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..f5ca6389b8ff5 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -0,0 +1,34 @@ +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( + 'name' => 'My Font Collection', + 'description' => 'Demo about how to a font collection to your WordPress Font Library.', + 'font_families' => array( 'mock' ), + ); + + WP_Font_Library::get_instance()->register_font_collection( 'my-font-collection', $my_font_collection_config ); + + $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/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..d3b0f126e2e7e --- /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::get_instance()->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::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. + WP_Font_Library::get_instance()->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..ddb0fa91c1d60 --- /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::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::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::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. + $this->assertEmpty( $collections, 'Font collections were not unregistered.' ); + } + + public function unregister_non_existing_collection() { + // 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/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.' ); + } +}