wp/wp-includes/fonts/class-wp-font-face.php
changeset 21 48c4eec2b7e6
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * WP_Font_Face class.
       
     4  *
       
     5  * @package    WordPress
       
     6  * @subpackage Fonts
       
     7  * @since      6.4.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Font Face generates and prints `@font-face` styles for given fonts.
       
    12  *
       
    13  * @since 6.4.0
       
    14  */
       
    15 class WP_Font_Face {
       
    16 
       
    17 	/**
       
    18 	 * The font-face property defaults.
       
    19 	 *
       
    20 	 * @since 6.4.0
       
    21 	 *
       
    22 	 * @var string[]
       
    23 	 */
       
    24 	private $font_face_property_defaults = array(
       
    25 		'font-family'  => '',
       
    26 		'font-style'   => 'normal',
       
    27 		'font-weight'  => '400',
       
    28 		'font-display' => 'fallback',
       
    29 	);
       
    30 
       
    31 	/**
       
    32 	 * Valid font-face property names.
       
    33 	 *
       
    34 	 * @since 6.4.0
       
    35 	 *
       
    36 	 * @var string[]
       
    37 	 */
       
    38 	private $valid_font_face_properties = array(
       
    39 		'ascent-override',
       
    40 		'descent-override',
       
    41 		'font-display',
       
    42 		'font-family',
       
    43 		'font-stretch',
       
    44 		'font-style',
       
    45 		'font-weight',
       
    46 		'font-variant',
       
    47 		'font-feature-settings',
       
    48 		'font-variation-settings',
       
    49 		'line-gap-override',
       
    50 		'size-adjust',
       
    51 		'src',
       
    52 		'unicode-range',
       
    53 	);
       
    54 
       
    55 	/**
       
    56 	 * Valid font-display values.
       
    57 	 *
       
    58 	 * @since 6.4.0
       
    59 	 *
       
    60 	 * @var string[]
       
    61 	 */
       
    62 	private $valid_font_display = array( 'auto', 'block', 'fallback', 'swap', 'optional' );
       
    63 
       
    64 	/**
       
    65 	 * Array of font-face style tag's attribute(s)
       
    66 	 * where the key is the attribute name and the
       
    67 	 * value is its value.
       
    68 	 *
       
    69 	 * @since 6.4.0
       
    70 	 *
       
    71 	 * @var string[]
       
    72 	 */
       
    73 	private $style_tag_attrs = array();
       
    74 
       
    75 	/**
       
    76 	 * Creates and initializes an instance of WP_Font_Face.
       
    77 	 *
       
    78 	 * @since 6.4.0
       
    79 	 */
       
    80 	public function __construct() {
       
    81 		if (
       
    82 			function_exists( 'is_admin' ) && ! is_admin()
       
    83 			&&
       
    84 			function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' )
       
    85 		) {
       
    86 			$this->style_tag_attrs = array( 'type' => 'text/css' );
       
    87 		}
       
    88 	}
       
    89 
       
    90 	/**
       
    91 	 * Generates and prints the `@font-face` styles for the given fonts.
       
    92 	 *
       
    93 	 * @since 6.4.0
       
    94 	 *
       
    95 	 * @param array[][] $fonts Optional. The font-families and their font variations.
       
    96 	 *                         See {@see wp_print_font_faces()} for the supported fields.
       
    97 	 *                         Default empty array.
       
    98 	 */
       
    99 	public function generate_and_print( array $fonts ) {
       
   100 		$fonts = $this->validate_fonts( $fonts );
       
   101 
       
   102 		// Bail out if there are no fonts are given to process.
       
   103 		if ( empty( $fonts ) ) {
       
   104 			return;
       
   105 		}
       
   106 
       
   107 		$css = $this->get_css( $fonts );
       
   108 
       
   109 		/*
       
   110 		 * The font-face CSS is contained within <style> tags and can only be interpreted
       
   111 		 * as CSS in the browser. Using wp_strip_all_tags() is sufficient escaping
       
   112 		 * to avoid malicious attempts to close </style> and open a <script>.
       
   113 		 */
       
   114 		$css = wp_strip_all_tags( $css );
       
   115 
       
   116 		// Bail out if there is no CSS to print.
       
   117 		if ( empty( $css ) ) {
       
   118 			return;
       
   119 		}
       
   120 
       
   121 		printf( $this->get_style_element(), $css );
       
   122 	}
       
   123 
       
   124 	/**
       
   125 	 * Validates each of the font-face properties.
       
   126 	 *
       
   127 	 * @since 6.4.0
       
   128 	 *
       
   129 	 * @param array $fonts The fonts to valid.
       
   130 	 * @return array Prepared font-faces organized by provider and font-family.
       
   131 	 */
       
   132 	private function validate_fonts( array $fonts ) {
       
   133 		$validated_fonts = array();
       
   134 
       
   135 		foreach ( $fonts as $font_faces ) {
       
   136 			foreach ( $font_faces as $font_face ) {
       
   137 				$font_face = $this->validate_font_face_declarations( $font_face );
       
   138 				// Skip if failed validation.
       
   139 				if ( false === $font_face ) {
       
   140 					continue;
       
   141 				}
       
   142 
       
   143 				$validated_fonts[] = $font_face;
       
   144 			}
       
   145 		}
       
   146 
       
   147 		return $validated_fonts;
       
   148 	}
       
   149 
       
   150 	/**
       
   151 	 * Validates each font-face declaration (property and value pairing).
       
   152 	 *
       
   153 	 * @since 6.4.0
       
   154 	 *
       
   155 	 * @param array $font_face Font face property and value pairings to validate.
       
   156 	 * @return array|false Validated font-face on success, or false on failure.
       
   157 	 */
       
   158 	private function validate_font_face_declarations( array $font_face ) {
       
   159 		$font_face = wp_parse_args( $font_face, $this->font_face_property_defaults );
       
   160 
       
   161 		// Check the font-family.
       
   162 		if ( empty( $font_face['font-family'] ) || ! is_string( $font_face['font-family'] ) ) {
       
   163 			// @todo replace with `wp_trigger_error()`.
       
   164 			_doing_it_wrong(
       
   165 				__METHOD__,
       
   166 				__( 'Font font-family must be a non-empty string.' ),
       
   167 				'6.4.0'
       
   168 			);
       
   169 			return false;
       
   170 		}
       
   171 
       
   172 		// Make sure that local fonts have 'src' defined.
       
   173 		if ( empty( $font_face['src'] ) || ( ! is_string( $font_face['src'] ) && ! is_array( $font_face['src'] ) ) ) {
       
   174 			// @todo replace with `wp_trigger_error()`.
       
   175 			_doing_it_wrong(
       
   176 				__METHOD__,
       
   177 				__( 'Font src must be a non-empty string or an array of strings.' ),
       
   178 				'6.4.0'
       
   179 			);
       
   180 			return false;
       
   181 		}
       
   182 
       
   183 		// Validate the 'src' property.
       
   184 		foreach ( (array) $font_face['src'] as $src ) {
       
   185 			if ( empty( $src ) || ! is_string( $src ) ) {
       
   186 				// @todo replace with `wp_trigger_error()`.
       
   187 				_doing_it_wrong(
       
   188 					__METHOD__,
       
   189 					__( 'Each font src must be a non-empty string.' ),
       
   190 					'6.4.0'
       
   191 				);
       
   192 				return false;
       
   193 			}
       
   194 		}
       
   195 
       
   196 		// Check the font-weight.
       
   197 		if ( ! is_string( $font_face['font-weight'] ) && ! is_int( $font_face['font-weight'] ) ) {
       
   198 			// @todo replace with `wp_trigger_error()`.
       
   199 			_doing_it_wrong(
       
   200 				__METHOD__,
       
   201 				__( 'Font font-weight must be a properly formatted string or integer.' ),
       
   202 				'6.4.0'
       
   203 			);
       
   204 			return false;
       
   205 		}
       
   206 
       
   207 		// Check the font-display.
       
   208 		if ( ! in_array( $font_face['font-display'], $this->valid_font_display, true ) ) {
       
   209 			$font_face['font-display'] = $this->font_face_property_defaults['font-display'];
       
   210 		}
       
   211 
       
   212 		// Remove invalid properties.
       
   213 		foreach ( $font_face as $property => $value ) {
       
   214 			if ( ! in_array( $property, $this->valid_font_face_properties, true ) ) {
       
   215 				unset( $font_face[ $property ] );
       
   216 			}
       
   217 		}
       
   218 
       
   219 		return $font_face;
       
   220 	}
       
   221 
       
   222 	/**
       
   223 	 * Gets the style element for wrapping the `@font-face` CSS.
       
   224 	 *
       
   225 	 * @since 6.4.0
       
   226 	 *
       
   227 	 * @return string The style element.
       
   228 	 */
       
   229 	private function get_style_element() {
       
   230 		$attributes = $this->generate_style_element_attributes();
       
   231 
       
   232 		return "<style id='wp-fonts-local'{$attributes}>\n%s\n</style>\n";
       
   233 	}
       
   234 
       
   235 	/**
       
   236 	 * Gets the defined <style> element's attributes.
       
   237 	 *
       
   238 	 * @since 6.4.0
       
   239 	 *
       
   240 	 * @return string A string of attribute=value when defined, else, empty string.
       
   241 	 */
       
   242 	private function generate_style_element_attributes() {
       
   243 		$attributes = '';
       
   244 		foreach ( $this->style_tag_attrs as $name => $value ) {
       
   245 			$attributes .= " {$name}='{$value}'";
       
   246 		}
       
   247 		return $attributes;
       
   248 	}
       
   249 
       
   250 	/**
       
   251 	 * Gets the `@font-face` CSS styles for locally-hosted font files.
       
   252 	 *
       
   253 	 * This method does the following processing tasks:
       
   254 	 *    1. Orchestrates an optimized `src` (with format) for browser support.
       
   255 	 *    2. Generates the `@font-face` for all its fonts.
       
   256 	 *
       
   257 	 * @since 6.4.0
       
   258 	 *
       
   259 	 * @param array[] $font_faces The font-faces to generate @font-face CSS styles.
       
   260 	 * @return string The `@font-face` CSS styles.
       
   261 	 */
       
   262 	private function get_css( $font_faces ) {
       
   263 		$css = '';
       
   264 
       
   265 		foreach ( $font_faces as $font_face ) {
       
   266 				// Order the font's `src` items to optimize for browser support.
       
   267 				$font_face = $this->order_src( $font_face );
       
   268 
       
   269 				// Build the @font-face CSS for this font.
       
   270 				$css .= '@font-face{' . $this->build_font_face_css( $font_face ) . '}' . "\n";
       
   271 		}
       
   272 
       
   273 		// Don't print the last newline character.
       
   274 		return rtrim( $css, "\n" );
       
   275 	}
       
   276 
       
   277 	/**
       
   278 	 * Orders `src` items to optimize for browser support.
       
   279 	 *
       
   280 	 * @since 6.4.0
       
   281 	 *
       
   282 	 * @param array $font_face Font face to process.
       
   283 	 * @return array Font-face with ordered src items.
       
   284 	 */
       
   285 	private function order_src( array $font_face ) {
       
   286 		if ( ! is_array( $font_face['src'] ) ) {
       
   287 			$font_face['src'] = (array) $font_face['src'];
       
   288 		}
       
   289 
       
   290 		$src         = array();
       
   291 		$src_ordered = array();
       
   292 
       
   293 		foreach ( $font_face['src'] as $url ) {
       
   294 			// Add data URIs first.
       
   295 			if ( str_starts_with( trim( $url ), 'data:' ) ) {
       
   296 				$src_ordered[] = array(
       
   297 					'url'    => $url,
       
   298 					'format' => 'data',
       
   299 				);
       
   300 				continue;
       
   301 			}
       
   302 			$format         = pathinfo( $url, PATHINFO_EXTENSION );
       
   303 			$src[ $format ] = $url;
       
   304 		}
       
   305 
       
   306 		// Add woff2.
       
   307 		if ( ! empty( $src['woff2'] ) ) {
       
   308 			$src_ordered[] = array(
       
   309 				'url'    => $src['woff2'],
       
   310 				'format' => 'woff2',
       
   311 			);
       
   312 		}
       
   313 
       
   314 		// Add woff.
       
   315 		if ( ! empty( $src['woff'] ) ) {
       
   316 			$src_ordered[] = array(
       
   317 				'url'    => $src['woff'],
       
   318 				'format' => 'woff',
       
   319 			);
       
   320 		}
       
   321 
       
   322 		// Add ttf.
       
   323 		if ( ! empty( $src['ttf'] ) ) {
       
   324 			$src_ordered[] = array(
       
   325 				'url'    => $src['ttf'],
       
   326 				'format' => 'truetype',
       
   327 			);
       
   328 		}
       
   329 
       
   330 		// Add eot.
       
   331 		if ( ! empty( $src['eot'] ) ) {
       
   332 			$src_ordered[] = array(
       
   333 				'url'    => $src['eot'],
       
   334 				'format' => 'embedded-opentype',
       
   335 			);
       
   336 		}
       
   337 
       
   338 		// Add otf.
       
   339 		if ( ! empty( $src['otf'] ) ) {
       
   340 			$src_ordered[] = array(
       
   341 				'url'    => $src['otf'],
       
   342 				'format' => 'opentype',
       
   343 			);
       
   344 		}
       
   345 		$font_face['src'] = $src_ordered;
       
   346 
       
   347 		return $font_face;
       
   348 	}
       
   349 
       
   350 	/**
       
   351 	 * Builds the font-family's CSS.
       
   352 	 *
       
   353 	 * @since 6.4.0
       
   354 	 *
       
   355 	 * @param array $font_face Font face to process.
       
   356 	 * @return string This font-family's CSS.
       
   357 	 */
       
   358 	private function build_font_face_css( array $font_face ) {
       
   359 		$css = '';
       
   360 
       
   361 		/*
       
   362 		 * Wrap font-family in quotes if it contains spaces
       
   363 		 * and is not already wrapped in quotes.
       
   364 		 */
       
   365 		if (
       
   366 			str_contains( $font_face['font-family'], ' ' ) &&
       
   367 			! str_contains( $font_face['font-family'], '"' ) &&
       
   368 			! str_contains( $font_face['font-family'], "'" )
       
   369 		) {
       
   370 			$font_face['font-family'] = '"' . $font_face['font-family'] . '"';
       
   371 		}
       
   372 
       
   373 		foreach ( $font_face as $key => $value ) {
       
   374 			// Compile the "src" parameter.
       
   375 			if ( 'src' === $key ) {
       
   376 				$value = $this->compile_src( $value );
       
   377 			}
       
   378 
       
   379 			// If font-variation-settings is an array, convert it to a string.
       
   380 			if ( 'font-variation-settings' === $key && is_array( $value ) ) {
       
   381 				$value = $this->compile_variations( $value );
       
   382 			}
       
   383 
       
   384 			if ( ! empty( $value ) ) {
       
   385 				$css .= "$key:$value;";
       
   386 			}
       
   387 		}
       
   388 
       
   389 		return $css;
       
   390 	}
       
   391 
       
   392 	/**
       
   393 	 * Compiles the `src` into valid CSS.
       
   394 	 *
       
   395 	 * @since 6.4.0
       
   396 	 *
       
   397 	 * @param array $value Value to process.
       
   398 	 * @return string The CSS.
       
   399 	 */
       
   400 	private function compile_src( array $value ) {
       
   401 		$src = '';
       
   402 
       
   403 		foreach ( $value as $item ) {
       
   404 			$src .= ( 'data' === $item['format'] )
       
   405 				? ", url({$item['url']})"
       
   406 				: ", url('{$item['url']}') format('{$item['format']}')";
       
   407 		}
       
   408 
       
   409 		$src = ltrim( $src, ', ' );
       
   410 		return $src;
       
   411 	}
       
   412 
       
   413 	/**
       
   414 	 * Compiles the font variation settings.
       
   415 	 *
       
   416 	 * @since 6.4.0
       
   417 	 *
       
   418 	 * @param array $font_variation_settings Array of font variation settings.
       
   419 	 * @return string The CSS.
       
   420 	 */
       
   421 	private function compile_variations( array $font_variation_settings ) {
       
   422 		$variations = '';
       
   423 
       
   424 		foreach ( $font_variation_settings as $key => $value ) {
       
   425 			$variations .= "$key $value";
       
   426 		}
       
   427 
       
   428 		return $variations;
       
   429 	}
       
   430 }