mirror of
https://github.com/WordPress/create-block-theme.git
synced 2026-07-27 21:48:03 +08:00
* tests: add tiny.png and evil.php.txt fixtures for media validation * docs(agents): update test:php commands to test:unit:php * Add CBT_Theme_Media::is_allowed_media_url() extension allowlist * Add CBT_Theme_Media::is_allowed_media_file() MIME allowlist * Apply media URL + file allowlist in add_media_to_local * Add CBT_Theme_Fonts::is_allowed_font_url() extension allowlist * Add CBT_Theme_Fonts::is_allowed_font_file() MIME allowlist * Apply font URL + file allowlist in copy_font_assets_to_theme * Allow font/sfnt and application/vnd.ms-opentype for TTF/OTF MIME detection * Reject multi-extension polyglots in URL allowlist * Apply media+font allowlist to zip export sinks in theme-zip * Add positive end-to-end tests for media and font sinks * Verify downloaded fonts by magic bytes instead of libmagic MIME * Pass $font_src string to download_url instead of $font_face src array Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Stream downloaded media into zip and clean up tmp file Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Strip query string before deriving folder path from media URL Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Restore braces and is_wp_error check dropped by Copilot suggestions Two recent Copilot suggestion commits accidentally removed structural code while making single-line changes: -c045da7("Pass $font_src to download_url") dropped the `} else {` and the `is_wp_error( $tmp_file )` guard. -617f6ec("Stream downloaded media into zip") dropped the closing brace of the `foreach` in add_media_to_zip(). Both broke PHP parse on every CI matrix. Restoring the missing statements so the file is valid again. * lint: align assignment operators in get_media_folder_path_from_url * Strip query string from filename before renaming downloaded media Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Strip query string from font URL before deriving filename Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Strip query string from font URL before deriving filename in zip Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Restore loop brace and sanitize_title dropped by Copilot suggestions Two more Copilot suggestion commits dropped surrounding context lines: -ad31fe4("Strip query string from filename before renaming downloaded media") removed the closing brace of the foreach loop in add_media_to_local(), causing fatal lint errors. -860f1bc("Strip query string from font URL before deriving filename in zip") removed the `$font_family_dir_name = sanitize_title(...)` assignment, leaving an undefined variable used immediately below. * Remove dead pre-loop assignments in add_activated_fonts_to_zip `$font_filename = basename( $font_face['src'] )` threw TypeError on PHP 8+ when src was already an array (valid per Theme JSON spec) and was redefined inside the inner src loop anyway. The adjacent `$font_dir = wp_get_font_dir()` was likewise redundant — also redefined inside the inner loop. Removing both. * Verify downloaded media by magic bytes instead of libmagic MIME wp_check_filetype_and_ext() was inconsistent with the URL allowlist: SVG isn't in WP Core's default mime registry at all, and WP maps wmv to video/x-ms-wmv / avi to video/avi while the post-check allowlist had only video/x-msvideo. Legitimate SVG/WMV/AVI URLs passed preflight and then silently failed post-download. Switching media to magic-byte verification (matching the font sink) removes the dependency on WP Core's mime registry and libmagic versions. Recognised formats: JPEG, PNG, GIF, WebP, SVG, MP4/M4V/MOV/ 3GP/3G2 (via ftyp), WebM, OGV, WMV (ASF), AVI (RIFF), MPEG. * Add regression tests for media asset validation * Fix media validation bugs found by scruffian Three fixes for issues found via regression tests in8da66ff: 1. is_allowed_media_file() now requires the downloaded bytes to match the URL extension specifically — a .jpg URL with SVG/MP4/anything-else bytes is rejected. Previously any allowed magic was accepted, so a browser MIME-sniffing the on-disk .jpg containing SVG could render it as SVG (with all the script-execution surface that brings). 2. make_relative_media_url() now derives the filename from the parsed URL path instead of raw basename(). A URL like cat.jpg?/evil.php previously produced `/assets/images/evil.php` in exported template markup because basename() treats query-string slashes as path separators. 3. add_media_to_zip() reads the downloaded bytes into memory and unlinks the tmp file BEFORE adding to the archive via addFromStringToTheme(). The previous `addFileToTheme() + unlink` sequence broke because ZipArchive defers reading files added via addFile() until close() — by which time the tmp file was gone. * lint: suppress NoSilencedErrors warning on ZipArchive::close in test * Skip zip tests gracefully when ZipArchive extension is unavailable Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Guard localhost retry against missing host/port keys parse_url() omits the 'port' key when the URL has no explicit port (e.g. `http://localhost/foo`). The retry-on-localhost block in both add_media_to_zip() and add_media_to_local() read $parsed_url['port'] unconditionally, raising an undefined-index notice when the initial download failed for such a URL. Guard with isset() on both keys. * Match font magic bytes against URL extension specifically Previously any recognised font magic was accepted, so a .woff2 URL returning TTF/WOFF/OTF/EOT bytes would pass and be saved under the .woff2 filename. This defeated the extension+MIME allowlist intent and would produce broken assets in the exported theme/zip (browsers would fail to load a WOFF2-named file containing TTF content). The check now switches on the URL extension and validates the magic matches THAT specific format. Mirrors the same fix already applied to is_allowed_media_file(). * Align .mpeg handling across URL, folder, and file-content checks is_allowed_media_file() accepted both .mpg and .mpeg, but is_allowed_media_url() and get_media_folder_path_from_url() only listed .mpg. A template with a .mpeg video would get rewritten to a local asset URL, then skipped before download — leaving the exported theme pointing at a missing file. Add .mpeg to the URL allowlist and the folder mapping so all three lists agree (matching WP Core's wp_get_mime_types which maps mpeg|mpg|mpe to video/mpeg). Also drops a pre-existing duplicate 'ogv' entry in the folder mapping that was noticed while editing. * Stream font bytes into zip before unlinking tmp file Same fix as add_media_to_zip: ZipArchive::addFile() defers reading the file until close() is called, so unlinking the download_url tmp file immediately after addFileToTheme() left an empty entry in the final archive when the zip was finalised. Read the bytes into memory, unlink, then addFromStringToTheme() — mirrors the pattern in add_media_to_zip(). Only the remote-download branch is affected; the local-copy branch points at a permanent file in the font library so its addFileToTheme() call stays. * Tweak comment * Allow AVIF images alongside JPEG/PNG/GIF/WebP/SVG AVIF is a WordPress-supported image format but was missing from the URL allowlist, folder mapping, and magic-byte check. The result: make_template_images_local() rewrote .avif URLs to local references, but the post-rewrite download was rejected — leaving exported themes pointing at missing assets. AVIF uses the ISO BMFF container (like MP4) with 'avif' or 'avis' as the major brand at offset 8. Added the brand-specific magic check alongside the URL/folder allowlist entries, plus tests for both AVIF still-image ('avif') and image-sequence ('avis') brands. * Drop rejected font sources from exported families Previously a font source that failed the URL allowlist or post-download MIME check was skipped via continue, but the original $font_src stayed in $font_face['src']. copy_activated_fonts_to_theme() then merged that family into theme.json — leaving the user's activated font pointing at an uncopied (and possibly disallowed) source while the user custom settings were cleared. Both copy_font_assets_to_theme() and add_activated_fonts_to_zip() now build a fresh srcs list and only retain entries that successfully copied or were already file:./ asset paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Only rewrite media URLs after successful asset validation * Accept AVIF compatible-brands in addition to the major brand Some AVIF files (typically those emitted by HEIF-derived tooling) set the major brand to mif1/miaf and only list avif/avis in the compatible brands. Parsing only the major brand at offset 8 would false-reject them. Scan the full FileTypeBox brand list (major brand plus compatible brands at offsets 16+) for any AVIF brand. HEIC files (no avif/avis anywhere) are still rejected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Magic-byte check the local-copy font branch too Both copy_font_assets_to_theme() and add_activated_fonts_to_zip() treated a font src under the WP user-fonts directory as trusted — copying / zipping the bytes without ever inspecting them. Anything that ended up in that directory through a separate flow (a polyglot upload via another plugin, manual write, etc.) would be promoted to the exported theme as-is just because the URL extension was on the allowlist. Run the same is_allowed_font_file() check on the local source before copying / zipping. If the bytes don't match the URL extension's font format, drop the source from the returned families. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Localize media URLs via the block parser, not str_replace Plain str_replace on the whole template would substring-match a shorter validated URL inside a longer rejected URL when one is a prefix of the other (e.g. `photo.png` inside `photo.png.php`). That silently rewrote the rejected URL to a missing local asset and bypassed the validated-media guard. Walk the parsed block tree instead: WP_HTML_Tag_Processor handles img / video src+poster and inline `background-image:url(...)`, while direct array access handles block-comment JSON attrs. Replacements only touch values that exactly match a validated URL. The rewrites embed literal PHP open/close tags that must end up verbatim in the export, but both set_attribute() and wp_json_encode() (inside serialize_blocks) would escape `<` / `>` / quotes. Route the rewrites through opaque, URL-shaped placeholders so they survive both passes, then strtr() them back to the raw PHP at the end. The placeholder is a root-relative path because set_attribute prefixes schemeless values with `http://`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Dwyer <ben@scruffian.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
443 lines
15 KiB
PHP
443 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Theme Fonts
|
|
*
|
|
* @package Create_Block_Theme
|
|
*/
|
|
class CBT_Theme_Fonts {
|
|
|
|
/**
|
|
* Make the font face theme src urls absolute.
|
|
*
|
|
* It replaces the 'file:./' prefix with the theme directory uri.
|
|
*
|
|
* Example: 'file:./assets/fonts/my-font.ttf' -> 'http://example.com/wp-content/themes/my-theme/assets/fonts/my-font.ttf'
|
|
* Example: [ 'https://example.com/assets/fonts/my-font.ttf' ] -> [ 'https://example.com/assets/fonts/my-font.ttf' ]
|
|
*
|
|
* @param array|string $src
|
|
* @return array|string
|
|
*/
|
|
private static function make_theme_font_src_absolute( $src ) {
|
|
$make_absolute = function( $url ) {
|
|
if ( str_starts_with( $url, 'file:./' ) ) {
|
|
return str_replace( 'file:./', get_stylesheet_directory_uri() . '/', $url );
|
|
}
|
|
return $url;
|
|
};
|
|
|
|
if ( is_array( $src ) ) {
|
|
return array_map( $make_absolute, $src );
|
|
}
|
|
|
|
return $make_absolute( $src );
|
|
}
|
|
|
|
/**
|
|
* Get all fonts from the theme.json data + all the style variations.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_all_fonts() {
|
|
$font_families = array();
|
|
$theme = CBT_Theme_JSON_Resolver::get_merged_data();
|
|
$settings = $theme->get_settings();
|
|
|
|
if ( isset( $settings['typography']['fontFamilies']['theme'] ) ) {
|
|
$font_families = array_merge( $font_families, $settings['typography']['fontFamilies']['theme'] );
|
|
}
|
|
|
|
if ( isset( $settings['typography']['fontFamilies']['custom'] ) ) {
|
|
$font_families = array_merge( $font_families, $settings['typography']['fontFamilies']['custom'] );
|
|
}
|
|
|
|
$variations = CBT_Theme_JSON_Resolver::get_style_variations();
|
|
|
|
foreach ( $variations as $variation ) {
|
|
if ( isset( $variation['settings']['typography']['fontFamilies']['theme'] ) ) {
|
|
$font_families = array_merge( $font_families, $variation['settings']['typography']['fontFamilies']['theme'] );
|
|
}
|
|
}
|
|
|
|
// Iterates through the font families and makes the urls absolute to use in the frontend code.
|
|
foreach ( $font_families as &$font_family ) {
|
|
if ( isset( $font_family['fontFace'] ) ) {
|
|
foreach ( $font_family['fontFace'] as &$font_face ) {
|
|
$font_face['src'] = self::make_theme_font_src_absolute( $font_face['src'] );
|
|
}
|
|
}
|
|
}
|
|
|
|
return $font_families;
|
|
}
|
|
|
|
/**
|
|
* Copy any ACTIVATED fonts from USER configuration to THEME configuration including any font face assets.
|
|
* Remove any DEACTIVATED fonts from the THEME configuration.
|
|
*/
|
|
public static function persist_font_settings() {
|
|
self::remove_deactivated_fonts_from_theme();
|
|
self::copy_activated_fonts_to_theme();
|
|
}
|
|
|
|
public static function get_user_activated_fonts() {
|
|
$user_settings = CBT_Theme_JSON_Resolver::get_user_data()->get_settings();
|
|
return $user_settings['typography']['fontFamilies']['custom'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Make a pretty filename from a font face.
|
|
*
|
|
* The filename is based on the font family name, weight, style, unicode range and the source index.
|
|
* Example:
|
|
* $font_face = [ 'fontFamily' => 'Open Sans', 'fontWeight' => '400', 'fontStyle' => 'normal' ]
|
|
* $src = 'https://example.com/assets/fonts/open-sans-regular.ttf'
|
|
* $src_index = 0
|
|
* Returns: 'open-sans-400-normal.ttf'
|
|
*
|
|
* @param array $font_face
|
|
* @param string $src
|
|
* @param int $src_index
|
|
* @return string
|
|
*/
|
|
public static function make_filename_from_fontface( $font_face, $src, $src_index = 0 ) {
|
|
$font_extension = pathinfo( $src, PATHINFO_EXTENSION );
|
|
$font_filename = sanitize_title( $font_face['fontFamily'] )
|
|
. ( isset( $font_face['fontWeight'] ) ? '-' . sanitize_title( $font_face['fontWeight'] ) : '' )
|
|
. ( isset( $font_face['fontStyle'] ) ? '-' . sanitize_title( $font_face['fontStyle'] ) : '' )
|
|
. ( isset( $font_face['unicodeRange'] ) ? '-' . sanitize_title( $font_face['unicodeRange'] ) : '' )
|
|
. ( 0 !== $src_index ? '-' . $src_index : '' )
|
|
. '.'
|
|
. $font_extension;
|
|
|
|
return $font_filename;
|
|
}
|
|
|
|
/**
|
|
* Allowlist check on the URL's path extension before we attempt to download a font.
|
|
*
|
|
* Defends against multi-extension polyglots (`evil.php.woff2`) by rejecting
|
|
* any URL whose basename contains a dangerous extension segment anywhere,
|
|
* not just at the end.
|
|
*
|
|
* @param string $url Absolute URL pointing at a font face source.
|
|
* @return bool True if the basename is safe and the final extension is in the font allowlist.
|
|
*/
|
|
public static function is_allowed_font_url( $url ) {
|
|
if ( ! is_string( $url ) || '' === $url ) {
|
|
return false;
|
|
}
|
|
$path = wp_parse_url( $url, PHP_URL_PATH );
|
|
$basename = strtolower( basename( (string) $path ) );
|
|
|
|
// Reject if ANY dot-separated segment is a dangerous extension.
|
|
// Mirrors the denylist in CBT_Theme_Media::is_allowed_media_url().
|
|
$dangerous = array(
|
|
'php',
|
|
'phtml',
|
|
'phar',
|
|
'php3',
|
|
'php4',
|
|
'php5',
|
|
'php7',
|
|
'php8',
|
|
'phps',
|
|
'html',
|
|
'htm',
|
|
'xhtml',
|
|
'htaccess',
|
|
'htpasswd',
|
|
'cgi',
|
|
'pl',
|
|
'py',
|
|
'rb',
|
|
'sh',
|
|
'asp',
|
|
'aspx',
|
|
'jsp',
|
|
'js',
|
|
'mjs',
|
|
);
|
|
foreach ( explode( '.', $basename ) as $segment ) {
|
|
if ( in_array( $segment, $dangerous, true ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$extension = pathinfo( $basename, PATHINFO_EXTENSION );
|
|
$allowed = array( 'ttf', 'otf', 'woff', 'woff2', 'eot' );
|
|
return in_array( $extension, $allowed, true );
|
|
}
|
|
|
|
/**
|
|
* Magic-byte verification of a downloaded font file against its URL extension.
|
|
*
|
|
* The downloaded bytes must match the format claimed by the URL extension —
|
|
* a `.woff2` URL whose body is TTF (or anything else) is rejected so we
|
|
* never persist content that doesn't match its filename on disk.
|
|
*
|
|
* libmagic-based MIME detection (via finfo or wp_check_filetype_and_ext)
|
|
* is unreliable for font formats: WordPress Core has no font MIMEs in its
|
|
* registry, and PHP base images ship with varying libmagic versions.
|
|
* Verifying magic bytes directly is version-independent and gives a
|
|
* stronger guarantee.
|
|
*
|
|
* Recognised magic per extension:
|
|
* - woff2 → `wOF2` at offset 0
|
|
* - woff → `wOFF` at offset 0
|
|
* - otf → `OTTO` at offset 0
|
|
* - ttf → `\x00\x01\x00\x00` at offset 0 (or `true` for legacy Mac)
|
|
* - eot → Version field (offset 8) is 0x00010000 / 0x00020001 / 0x00020002
|
|
*
|
|
* @param string $tmp_file Local path to the downloaded file.
|
|
* @param string $url The originating URL — its extension determines
|
|
* which magic-byte family the body must match.
|
|
* @return bool True if the file's leading bytes match the magic signature
|
|
* expected for the URL's extension.
|
|
*/
|
|
public static function is_allowed_font_file( $tmp_file, $url ) {
|
|
if ( ! is_string( $tmp_file ) || ! file_exists( $tmp_file ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Read 12 bytes — covers magic-at-offset-0 formats (4 bytes) and the
|
|
// EOT Version field at offset 8 (4 bytes).
|
|
$fp = fopen( $tmp_file, 'rb' );
|
|
if ( false === $fp ) {
|
|
return false;
|
|
}
|
|
$head = fread( $fp, 12 );
|
|
fclose( $fp );
|
|
|
|
if ( false === $head || strlen( $head ) < 4 ) {
|
|
return false;
|
|
}
|
|
|
|
// Derive the extension from the URL's path (ignore query/fragment) so
|
|
// the bytes must match the format claimed by the URL — saving a TTF
|
|
// body under a .woff2 filename would otherwise defeat the allowlist
|
|
// intent and produce broken assets in the exported theme/zip.
|
|
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
|
|
$extension = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
|
|
$first_four = substr( $head, 0, 4 );
|
|
|
|
switch ( $extension ) {
|
|
case 'woff2':
|
|
return 'wOF2' === $first_four;
|
|
|
|
case 'woff':
|
|
return 'wOFF' === $first_four;
|
|
|
|
case 'otf':
|
|
return 'OTTO' === $first_four;
|
|
|
|
case 'ttf':
|
|
return "\x00\x01\x00\x00" === $first_four || 'true' === $first_four;
|
|
|
|
case 'eot':
|
|
return 12 === strlen( $head )
|
|
&& in_array(
|
|
substr( $head, 8, 4 ),
|
|
array( "\x00\x00\x01\x00", "\x01\x00\x02\x00", "\x02\x00\x02\x00" ),
|
|
true
|
|
);
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Copy the font assets to the theme.
|
|
*
|
|
* @param array $font_families The font families to copy.
|
|
* @return array $font_families The font families with the font face src updated to the theme font asset location.
|
|
*/
|
|
public static function copy_font_assets_to_theme( $font_families ) {
|
|
$theme_font_asset_location = path_join( get_stylesheet_directory(), 'assets/fonts/' );
|
|
// Create the font asset directory if it does not exist.
|
|
wp_mkdir_p( $theme_font_asset_location );
|
|
|
|
foreach ( $font_families as &$font_family ) {
|
|
if ( ! isset( $font_family['fontFace'] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$font_family_dir_name = sanitize_title( $font_family['name'] );
|
|
$font_family_dir_path = path_join( $theme_font_asset_location, $font_family_dir_name );
|
|
// Crete a font family specific directory if it does not exist.
|
|
wp_mkdir_p( $font_family_dir_path );
|
|
|
|
foreach ( $font_family['fontFace'] as &$font_face ) {
|
|
// src can be a string or an array
|
|
// if it is a string, cast it to an array
|
|
$font_face['src'] = (array) $font_face['src'];
|
|
// Build a fresh srcs list so rejected sources (disallowed URL,
|
|
// failed download, MIME mismatch) are dropped rather than
|
|
// persisted into theme.json.
|
|
$kept_srcs = array();
|
|
foreach ( $font_face['src'] as $font_src_index => $font_src ) {
|
|
if ( str_starts_with( $font_src, 'file:' ) ) {
|
|
// If the font source starts with 'file:' then it's already a theme asset.
|
|
$kept_srcs[] = $font_src;
|
|
continue;
|
|
}
|
|
|
|
// Pre-download URL extension allowlist — applies to both
|
|
// the local-copy and remote-download branches because the
|
|
// URL itself is the input we don't trust.
|
|
if ( ! self::is_allowed_font_url( $font_src ) ) {
|
|
continue;
|
|
}
|
|
|
|
$font_src_path = (string) wp_parse_url( $font_src, PHP_URL_PATH );
|
|
$font_filename = basename( $font_src_path );
|
|
$font_pretty_filename = self::make_filename_from_fontface( $font_face, $font_src_path, $font_src_index );
|
|
$font_face_path = path_join( $font_family_dir_path, $font_pretty_filename );
|
|
$font_dir = wp_get_font_dir();
|
|
if ( str_contains( $font_src, $font_dir['url'] ) ) {
|
|
// If the file is hosted on this server then copy it to the theme.
|
|
$local_source = path_join( $font_dir['path'], $font_filename );
|
|
// Magic-byte allowlist on the local source too — the
|
|
// WP user-fonts directory is the input we don't trust:
|
|
// anything that ended up there (via a separate flow,
|
|
// or a polyglot) shouldn't be copied verbatim into
|
|
// the theme just because the URL extension looked OK.
|
|
if ( ! self::is_allowed_font_file( $local_source, $font_src ) ) {
|
|
continue;
|
|
}
|
|
copy( $local_source, $font_face_path );
|
|
} else {
|
|
// otherwise download it from wherever it is hosted
|
|
$tmp_file = download_url( $font_src );
|
|
if ( is_wp_error( $tmp_file ) ) {
|
|
continue;
|
|
}
|
|
// Post-download MIME allowlist.
|
|
if ( ! self::is_allowed_font_file( $tmp_file, $font_src ) ) {
|
|
@unlink( $tmp_file );
|
|
continue;
|
|
}
|
|
copy( $tmp_file, $font_face_path );
|
|
unlink( $tmp_file );
|
|
}
|
|
$font_face_family_path = path_join( $font_family_dir_name, $font_pretty_filename );
|
|
$kept_srcs[] = path_join( 'file:./assets/fonts/', $font_face_family_path );
|
|
}
|
|
$font_face['src'] = $kept_srcs;
|
|
}
|
|
}
|
|
|
|
return $font_families;
|
|
}
|
|
|
|
public static function copy_activated_fonts_to_theme() {
|
|
$font_families_to_copy = self::get_user_activated_fonts();
|
|
|
|
if ( is_null( $font_families_to_copy ) ) {
|
|
return;
|
|
}
|
|
|
|
$theme_json = CBT_Theme_JSON_Resolver::get_theme_file_contents();
|
|
$copied_font_families = self::copy_font_assets_to_theme( $font_families_to_copy );
|
|
|
|
$theme_json['settings']['typography']['fontFamilies'] = array_merge(
|
|
$theme_json['settings']['typography']['fontFamilies'] ?? array(),
|
|
$copied_font_families
|
|
);
|
|
|
|
$user_settings = CBT_Theme_JSON_Resolver::get_user_data()->get_settings();
|
|
unset( $user_settings['typography']['fontFamilies']['custom'] );
|
|
if ( empty( $user_settings['typography']['fontFamilies'] ) ) {
|
|
unset( $user_settings['typography']['fontFamilies'] );
|
|
}
|
|
if ( empty( $user_settings['typography'] ) ) {
|
|
unset( $user_settings['typography'] );
|
|
}
|
|
|
|
CBT_Theme_JSON_Resolver::write_user_settings( $user_settings );
|
|
CBT_Theme_JSON_Resolver::write_theme_file_contents( $theme_json );
|
|
}
|
|
|
|
/**
|
|
* Remove font face assets from the theme that are not in the user configuration.
|
|
*
|
|
* @param array $font_families_to_not_remove
|
|
* @param array $theme_font_families
|
|
*/
|
|
private static function remove_deactivated_font_assets( $font_families_to_not_remove, $theme_font_families ) {
|
|
/* Bail if there are no theme font families, which can happen
|
|
* if the theme.json file, missing, or if the theme is a child theme, in
|
|
* which case the font families are inherited from the parent theme.
|
|
*/
|
|
if ( is_null( $theme_font_families ) ) {
|
|
return;
|
|
}
|
|
|
|
$theme_font_asset_location = get_stylesheet_directory() . '/assets/fonts/';
|
|
$font_families_to_remove = array_filter(
|
|
$theme_font_families,
|
|
function( $theme_font_family ) use ( $font_families_to_not_remove ) {
|
|
return ! in_array( $theme_font_family['slug'], array_column( $font_families_to_not_remove, 'slug' ), true );
|
|
}
|
|
);
|
|
|
|
foreach ( $font_families_to_remove as $font_family ) {
|
|
if ( isset( $font_family['fontFace'] ) ) {
|
|
foreach ( $font_family['fontFace'] as $font_face ) {
|
|
// src can be a string or an array
|
|
// if it is a string, cast it to an array
|
|
$srcs = (array) $font_face['src'];
|
|
foreach ( $srcs as $font_src ) {
|
|
$font_filename = basename( $font_src );
|
|
$file_path = $theme_font_asset_location . $font_filename;
|
|
if ( file_exists( $file_path ) ) {
|
|
unlink( $file_path );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove any deactivated fonts from the theme configuration.
|
|
* This includes removing the font face assets from the theme,
|
|
* but does not remove the font face assets from the user configuration.
|
|
*
|
|
* This is because the user may have deactivated a font, but still want to use it in the future.
|
|
*/
|
|
public static function remove_deactivated_fonts_from_theme() {
|
|
$user_settings = CBT_Theme_JSON_Resolver::get_user_data()->get_settings();
|
|
$theme_json = CBT_Theme_JSON_Resolver::get_theme_file_contents();
|
|
|
|
if ( ! isset( $user_settings['typography']['fontFamilies']['theme'] ) ) {
|
|
return;
|
|
}
|
|
|
|
$font_families_to_not_remove = $user_settings['typography']['fontFamilies']['theme'];
|
|
$theme_font_families = $theme_json['settings']['typography']['fontFamilies'] ?? null;
|
|
|
|
self::remove_deactivated_font_assets( $font_families_to_not_remove, $theme_font_families );
|
|
|
|
if ( ! is_null( $theme_font_families ) ) {
|
|
$theme_json['settings']['typography']['fontFamilies'] = array_filter(
|
|
$theme_font_families,
|
|
function( $theme_font_family ) use ( $font_families_to_not_remove ) {
|
|
return in_array( $theme_font_family['slug'], array_column( $font_families_to_not_remove, 'slug' ), true );
|
|
}
|
|
);
|
|
}
|
|
|
|
CBT_Theme_JSON_Resolver::write_theme_file_contents( $theme_json );
|
|
|
|
unset( $user_settings['typography']['fontFamilies']['theme'] );
|
|
if ( empty( $user_settings['typography']['fontFamilies'] ) ) {
|
|
unset( $user_settings['typography']['fontFamilies'] );
|
|
}
|
|
if ( empty( $user_settings['typography'] ) ) {
|
|
unset( $user_settings['typography'] );
|
|
}
|
|
|
|
CBT_Theme_JSON_Resolver::write_user_settings( $user_settings );
|
|
}
|
|
}
|