mirror of
https://github.com/WordPress/create-block-theme.git
synced 2026-07-26 21:42:48 +08:00
Some checks failed
Run checks / Compute previous WordPress version (push) Successful in 2s
Run checks / PHP 7.4 (push) Failing after 1s
Run checks / E2E Tests (push) Failing after 5s
Run checks / PHP 8.0 (push) Failing after 1s
Run checks / PHP 7.4 (WP previous major version) (push) Failing after 2s
Run checks / PHP 8.0 (WP previous major version) (push) Failing after 1s
Run checks / PHP 8.1 (push) Failing after 0s
Run checks / PHP 8.1 (WP previous major version) (push) Failing after 1s
Run checks / PHP 8.2 (push) Failing after 1s
Run checks / PHP 8.2 (WP previous major version) (push) Failing after 1s
Run checks / PHP 8.3 (push) Failing after 0s
Run checks / PHP 8.3 (WP previous major version) (push) Failing after 1s
Run checks / Lint (push) Failing after 19s
Add /theme-settings REST endpoint and CBT_Theme_Settings_Save service Server-side foundation for the Edit Theme Settings modal. New POST /create-block-theme/v1/theme-settings route persists a partial- theme.json payload from the modal into the active theme's theme.json. Behavior: - Validates payload shape (allowlisted top-level keys, JSON object/list enforcement, required entry keys, slug format). - Context-aware sanitization: text fields (name/title/label/description) go through `sanitize_text_field`; slug fields through `sanitize_key`; CSS-bearing values (shadow/color/gradient/fontFamily) through `wp_kses_no_nul`l so multi-segment CSS round-trips intact. - Deep-merges into the existing theme.json using JSON Merge Patch semantics (RFC 7396): null deletes a key, empty object is a no-op, empty list clears an existing list, lists replace wholesale. - Translates `removedShadowDefaults` into settings.shadow.defaultPresets: false plus the kept core defaults re-registered as customs. Preserves user-defined presets and is idempotent. - Atomic write with a per-request temp file + rename. wp_json_encode failure short-circuits before touching theme.json. - Concurrent saves are serialized via flock() on a temp-dir lockfile scoped by theme slug, so disjoint edits compose correctly. Returns the merged theme.json so the client can refresh without a second fetch. Capability gate: edit_theme_options. Part of #836. Closes #837.
200 lines
7.3 KiB
PHP
200 lines
7.3 KiB
PHP
<?php
|
|
|
|
function cbt_augment_resolver_with_utilities() {
|
|
|
|
//Ultimately it is desireable for Core to have this functionality natively.
|
|
// In the meantime we are patching the functionality we are expecting into the Theme JSON Resolver here
|
|
if ( ! class_exists( 'WP_Theme_JSON_Resolver' ) ) {
|
|
return;
|
|
}
|
|
|
|
class CBT_Theme_JSON_Resolver extends WP_Theme_JSON_Resolver {
|
|
|
|
/**
|
|
* Export the combined (and flattened) THEME and CUSTOM data.
|
|
*
|
|
* @param string $content ['all', 'current', 'user'] Determines which settings content to include in the export.
|
|
* @param array $extra_theme_data Any theme json extra data to be included in the export.
|
|
* All options include user settings.
|
|
* 'current' will include settings from the currently installed theme but NOT from the parent theme.
|
|
* 'all' will include settings from the current theme as well as the parent theme (if it has one)
|
|
* 'variation' will include just the user custom styles and settings.
|
|
*/
|
|
public static function export_theme_data( $content, $extra_theme_data = null ) {
|
|
$current_theme = wp_get_theme();
|
|
if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) {
|
|
$theme = new WP_Theme_JSON_Gutenberg();
|
|
} else {
|
|
$theme = new WP_Theme_JSON();
|
|
}
|
|
|
|
if ( 'all' === $content && $current_theme->parent() ) {
|
|
// Get parent theme.json.
|
|
$parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) );
|
|
$parent_theme_json_data = static::translate( $parent_theme_json_data, $current_theme->parent()->get( 'TextDomain' ) );
|
|
|
|
// Get the schema from the parent JSON.
|
|
$schema = $parent_theme_json_data['$schema'];
|
|
if ( array_key_exists( 'schema', $parent_theme_json_data ) ) {
|
|
$schema = $parent_theme_json_data['$schema'];
|
|
}
|
|
|
|
if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) {
|
|
$parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data );
|
|
} else {
|
|
$parent_theme = new WP_Theme_JSON( $parent_theme_json_data );
|
|
}
|
|
$theme->merge( $parent_theme );
|
|
}
|
|
|
|
if ( 'all' === $content || 'current' === $content ) {
|
|
$theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) );
|
|
$theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
|
|
|
|
// Get the schema from the parent JSON.
|
|
if ( array_key_exists( 'schema', $theme_json_data ) ) {
|
|
$schema = $theme_json_data['$schema'];
|
|
}
|
|
|
|
if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) {
|
|
$theme_theme = new WP_Theme_JSON_Gutenberg( $theme_json_data );
|
|
} else {
|
|
$theme_theme = new WP_Theme_JSON( $theme_json_data );
|
|
}
|
|
$theme->merge( $theme_theme );
|
|
}
|
|
|
|
// Merge the User Data
|
|
$theme->merge( static::get_user_data() );
|
|
|
|
// Merge the extra theme data received as a parameter
|
|
if ( ! empty( $extra_theme_data ) ) {
|
|
if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) {
|
|
$extra_data = new WP_Theme_JSON_Gutenberg( $extra_theme_data );
|
|
} else {
|
|
$extra_data = new WP_Theme_JSON( $extra_theme_data );
|
|
}
|
|
$theme->merge( $extra_data );
|
|
}
|
|
|
|
$data = $theme->get_data();
|
|
|
|
// Add the schema.
|
|
if ( empty( $schema ) ) {
|
|
global $wp_version;
|
|
$theme_json_version = 'wp/' . substr( $wp_version, 0, 3 );
|
|
if ( defined( 'IS_GUTENBERG_PLUGIN' ) ) {
|
|
$theme_json_version = 'trunk';
|
|
}
|
|
$schema = 'https://schemas.wp.org/' . $theme_json_version . '/theme.json';
|
|
}
|
|
// Ensure $schema is the first property and version is second in the JSON output.
|
|
$ordered_data = array( '$schema' => $schema );
|
|
|
|
// Add version as the second property if it exists.
|
|
if ( isset( $data['version'] ) ) {
|
|
$ordered_data['version'] = $data['version'];
|
|
unset( $data['version'] );
|
|
}
|
|
|
|
// Merge the remaining data.
|
|
$data = array_merge( $ordered_data, $data );
|
|
return static::stringify( $data );
|
|
}
|
|
|
|
/**
|
|
* Get the user data.
|
|
*
|
|
* This is a copy of the parent function with the addition of the Gutenberg resolver.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_user_data() {
|
|
// Determine the correct method to retrieve user data
|
|
return class_exists( 'WP_Theme_JSON_Resolver_Gutenberg' )
|
|
? WP_Theme_JSON_Resolver_Gutenberg::get_user_data()
|
|
: parent::get_user_data();
|
|
}
|
|
|
|
/**
|
|
* Stringify the array data.
|
|
*
|
|
* $data is an array of data to be converted to a JSON string.
|
|
* @return string JSON string.
|
|
*/
|
|
public static function stringify( $data ) {
|
|
$data = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
|
// Convert spaces to tabs
|
|
return preg_replace( '~(?:^|\G)\h{4}~m', "\t", $data );
|
|
}
|
|
|
|
public static function get_theme_file_contents() {
|
|
$theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) );
|
|
return $theme_json_data;
|
|
}
|
|
|
|
public static function write_theme_file_contents( $theme_json_data ) {
|
|
$theme_json = wp_json_encode( $theme_json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
|
// `wp_json_encode` returns false if the data contains an
|
|
// unencodable value (resources, NAN/INF after wrapping, etc.).
|
|
// Bail before opening a temp file so theme.json is never
|
|
// truncated or replaced with empty content.
|
|
if ( false === $theme_json ) {
|
|
return false;
|
|
}
|
|
$target = static::get_file_path_from_theme( 'theme.json' );
|
|
|
|
// Atomic write with a request-unique temp file: write to a
|
|
// per-request sibling temp file, then rename into place. A
|
|
// per-request name prevents two concurrent saves from clobbering
|
|
// each other's staging payload (a shared `theme.json.tmp` is unsafe
|
|
// — request A could rename request B's truncated contents, or one
|
|
// could unlink the other's temp file mid-write). Each request
|
|
// cleans up only its own temp file on failure.
|
|
$tmp = $target . '.' . uniqid( '', true ) . '.tmp';
|
|
// Suppress warnings so a permission/disk error returns false cleanly
|
|
// rather than emitting a PHP warning that may be promoted to an
|
|
// exception. Callers must check the boolean return value.
|
|
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
|
$bytes = @file_put_contents( $tmp, $theme_json );
|
|
if ( false === $bytes ) {
|
|
return false;
|
|
}
|
|
|
|
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
|
if ( ! @rename( $tmp, $target ) ) {
|
|
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
|
@unlink( $tmp );
|
|
return false;
|
|
}
|
|
|
|
static::clean_cached_data();
|
|
// Bust the WP-level theme cache too — `clean_cached_data()` only
|
|
// clears the JSON resolver's caches, not `wp_get_theme()`.
|
|
wp_get_theme()->cache_delete();
|
|
return true;
|
|
}
|
|
|
|
public static function write_user_settings( $user_settings ) {
|
|
$global_styles_id = static::get_user_global_styles_post_id();
|
|
$request = new WP_REST_Request( 'POST', '/wp/v2/global-styles/' . $global_styles_id );
|
|
$request->set_param( 'settings', $user_settings );
|
|
rest_do_request( $request );
|
|
static::clean_cached_data();
|
|
}
|
|
|
|
public static function clean_cached_data() {
|
|
parent::clean_cached_data();
|
|
|
|
if ( class_exists( 'WP_Theme_JSON_Resolver_Gutenberg' ) ) {
|
|
WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data();
|
|
}
|
|
|
|
//TODO: Clearing the cache should clear this too.
|
|
// Does this clear the Gutenberg equivalent?
|
|
static::$theme_json_file_cache = array();
|
|
}
|
|
}
|
|
}
|
|
|
|
add_action( 'plugins_loaded', 'cbt_augment_resolver_with_utilities' );
|