mirror of
https://github.com/WordPress/create-block-theme.git
synced 2026-07-26 21:42:48 +08:00
* Add can_modify_theme() permission helper with DISALLOW_FILE_* respect
* Wire 9 mutating REST routes to can_modify_theme()
* Add multisite permission tests (skip when not in multisite env)
* Keep DISALLOW_FILE_* authoritative over the cbt_file_mods_allowed filter
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Assert /font-families returns 200 SUCCESS, not just not-403
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Avoid constructor side effects in invoke_private test helper
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Grant super-admin on multisite to isolate file-mod hardening assertion
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Update docblock
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Restore docblock close marker dropped by Copilot suggestion
* Delegate file-mod check to WP Core's wp_is_file_mod_allowed
scruffian flagged that the previous implementation bypassed WordPress
Core's canonical `file_mod_allowed` filter — the mechanism hosts and
security plugins use to disable file modifications globally.
Now delegates the DISALLOW_FILE_MODS / file_mod_allowed branch to
wp_is_file_mod_allowed( 'create_block_theme_modify_theme' ). The
explicit DISALLOW_FILE_EDIT check is kept on top because Core's
helper does NOT cover that constant (it's specifically for the theme
file editor UI).
The cbt_file_mods_allowed filter remains as a test seam — it can only
further restrict, never re-enable, the policy decided by core.
* Add regression test for cbt filter not overriding core file_mod_allowed
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Use edit_themes + wp_is_file_mod_allowed for permission check
Refactors can_modify_theme() to compose two WordPress Core primitives
instead of reimplementing the same checks:
current_user_can( 'edit_themes' )
&& wp_is_file_mod_allowed( 'create_block_theme_modify_theme' )
The edit_themes capability already encodes the matrix this plugin
needs: held by Administrators on single-site, super-admins on multisite
(NOT sub-site admins), and automatically denied by Core when
DISALLOW_FILE_EDIT is defined. wp_is_file_mod_allowed handles
DISALLOW_FILE_MODS and the canonical file_mod_allowed filter.
That removes:
- the explicit is_multisite() / is_super_admin() branch
- the explicit DISALLOW_FILE_EDIT constant check
- the file_mods_allowed() helper
- the cbt_file_mods_allowed filter (was a test seam; tests now use
the canonical file_mod_allowed filter instead)
Functional matrix is unchanged.
* Document /reset-theme cap reuse for permission-surface consistency
* Hide UI when REST capability gate denies
Sub-site admins on multisite, DISALLOW_FILE_EDIT/DISALLOW_FILE_MODS
sites, and sites that deny file_mod_allowed for the
create_block_theme_modify_theme context now get 403s from every
mutating REST route — but the admin landing page and editor sidebar
were still registered behind edit_theme_options, so those users could
open the UI and click primary actions that the API rejects.
Make can_modify_theme() public static on CBT_Theme_API and gate
- the Appearance > Create Block Theme menu entry, and
- the site-editor plugin sidebar enqueue
on the same predicate, so the UI surface matches the REST surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add multisite specific test runs
* Activate a block theme in UI-gate tests
Without an active block theme, both create_admin_menu() and
create_block_theme_sidebar_enqueue() return early on the
wp_is_block_theme() check — so the prior negative-only assertions
would have passed even if the cap gate did nothing. Activate a blank
block theme via the plugin's /create-blank endpoint (mirroring the
helper used in test-theme-fonts.php), then assert both directions:
menu/script register with the gate open, and drop out once
file_mod_allowed denies. This proves the cap gate (not the block-theme
guard) is what hides the UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Don't invoke sidebar enqueue under gate-open in test
The sidebar enqueue includes build/plugin-sidebar.asset.php on the
gate-open path; the CI test job doesn't build the plugin before
running PHP tests, so the previous version of this test errored on
that include. The cap-gate behaviour we care about is the gate-denied
path — that short-circuits before the include.
Drop the gate-open enqueue call and replace it with direct asserts on
the two preconditions (wp_is_block_theme() is true, $pagenow is
site-editor.php). That still proves the cap gate (not one of the
earlier guards) is what dropped the script, without depending on
build artefacts that CI doesn't produce.
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: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
533 lines
16 KiB
PHP
533 lines
16 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/create-theme/resolver_additions.php';
|
|
require_once __DIR__ . '/create-theme/theme-locale.php';
|
|
require_once __DIR__ . '/create-theme/theme-tags.php';
|
|
require_once __DIR__ . '/create-theme/theme-zip.php';
|
|
require_once __DIR__ . '/create-theme/theme-media.php';
|
|
require_once __DIR__ . '/create-theme/theme-patterns.php';
|
|
require_once __DIR__ . '/create-theme/theme-templates.php';
|
|
require_once __DIR__ . '/create-theme/theme-styles.php';
|
|
require_once __DIR__ . '/create-theme/theme-json.php';
|
|
require_once __DIR__ . '/create-theme/theme-utils.php';
|
|
require_once __DIR__ . '/create-theme/theme-readme.php';
|
|
require_once __DIR__ . '/create-theme/theme-fonts.php';
|
|
require_once __DIR__ . '/create-theme/theme-create.php';
|
|
require_once __DIR__ . '/create-theme/theme-settings-save.php';
|
|
|
|
/**
|
|
* The api functionality of the plugin leveraged by the site editor UI.
|
|
*
|
|
* @package Create_Block_Theme
|
|
* @subpackage Create_Block_Theme/admin
|
|
* @author WordPress.org
|
|
*/
|
|
class CBT_Theme_API {
|
|
|
|
/**
|
|
* Initialize the class and set its properties.
|
|
*/
|
|
public function __construct() {
|
|
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
|
add_filter( 'rest_prepare_theme', array( $this, 'add_additional_data_to_theme_response' ), 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* Register the REST routes.
|
|
*/
|
|
public function register_rest_routes() {
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/export',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_export_theme' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/update',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_update_theme' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/save',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_save_theme' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/theme-settings',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_save_theme_settings' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/clone',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_clone_theme' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/create-variation',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_create_variation' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/create-blank',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_create_blank_theme' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/create-child',
|
|
array(
|
|
'methods' => 'POST',
|
|
'callback' => array( $this, 'rest_create_child_theme' ),
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
)
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/font-families',
|
|
array(
|
|
'methods' => 'GET',
|
|
'callback' => array( $this, 'rest_get_font_families' ),
|
|
'permission_callback' => function () {
|
|
return current_user_can( 'edit_theme_options' );
|
|
},
|
|
),
|
|
);
|
|
register_rest_route(
|
|
'create-block-theme/v1',
|
|
'/reset-theme',
|
|
array(
|
|
'methods' => WP_REST_Server::EDITABLE,
|
|
'callback' => array( $this, 'rest_reset_theme' ),
|
|
// /reset-theme doesn't mutate theme files (it only clears
|
|
// user customisations from the DB), but is gated on the same
|
|
// cap as the file-mutating routes for permission-surface
|
|
// consistency.
|
|
'permission_callback' => function () {
|
|
return self::can_modify_theme();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add README data and theme.json data to the current theme REST API response.
|
|
*
|
|
* @param WP_REST_Response $response The response object.
|
|
* @param WP_Theme $theme The theme object.
|
|
* @return WP_REST_Response Modified response object.
|
|
*/
|
|
function add_additional_data_to_theme_response( $response, $theme ) {
|
|
if ( ! $theme || ! $response ) {
|
|
return $response;
|
|
}
|
|
|
|
$current_theme = wp_get_theme();
|
|
if ( $theme->get_stylesheet() !== $current_theme->get_stylesheet() ) {
|
|
return $response;
|
|
}
|
|
|
|
$data = $response->get_data();
|
|
|
|
// Add README data and theme.json data.
|
|
try {
|
|
$data['readme'] = CBT_Theme_Readme::get_sections();
|
|
$data['theme_json'] = CBT_Theme_JSON_Resolver::get_theme_file_contents();
|
|
} catch ( Exception $error ) {
|
|
return new WP_Error(
|
|
'theme_data_retrieval_failed',
|
|
sprintf(
|
|
/* translators: %1$s: error message */
|
|
__( 'Failed to retrieve theme data: %1$s', 'create-block-theme' ),
|
|
$error->getMessage()
|
|
),
|
|
array(
|
|
'status' => 500,
|
|
'code' => $error->getCode(),
|
|
)
|
|
);
|
|
}
|
|
|
|
$response->set_data( $data );
|
|
|
|
return $response;
|
|
}
|
|
|
|
function rest_clone_theme( $request ) {
|
|
|
|
$response = CBT_Theme_Create::clone_current_theme( $this->sanitize_theme_data( $request->get_params() ) );
|
|
|
|
wp_cache_flush();
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Cloned Theme Created.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
function rest_create_child_theme( $request ) {
|
|
|
|
$theme = $this->sanitize_theme_data( $request->get_params() );
|
|
$theme['is_child_theme'] = true;
|
|
//TODO: Handle screenshots
|
|
$screenshot = null;
|
|
|
|
$response = CBT_Theme_Create::create_child_theme( $theme, $screenshot );
|
|
|
|
wp_cache_flush();
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Child Theme Created.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
function rest_create_variation( $request ) {
|
|
$options = $request->get_params();
|
|
|
|
$save_fonts = isset( $options['saveFonts'] ) && true === $options['saveFonts'];
|
|
|
|
$response = CBT_Theme_JSON::add_theme_json_variation_to_local(
|
|
'variation',
|
|
$this->sanitize_theme_data( $options ),
|
|
$save_fonts
|
|
);
|
|
|
|
wp_cache_flush();
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Theme Variation Created.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
function rest_create_blank_theme( $request ) {
|
|
|
|
$theme = $this->sanitize_theme_data( $request->get_params() );
|
|
//TODO: Handle screenshots
|
|
$screenshot = null;
|
|
|
|
$response = CBT_Theme_Create::create_blank_theme( $theme, $screenshot );
|
|
|
|
wp_cache_flush();
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Blank Theme Created.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Export the theme as a ZIP file.
|
|
*/
|
|
function rest_export_theme() {
|
|
if ( ! class_exists( 'ZipArchive' ) ) {
|
|
return new WP_Error(
|
|
'missing_zip_package',
|
|
__( 'Unable to create a zip file. ZipArchive not available.', 'create-block-theme' ),
|
|
);
|
|
}
|
|
wp_cache_flush();
|
|
$theme_slug = wp_get_theme()->get( 'TextDomain' );
|
|
|
|
// Create ZIP file in the temporary directory.
|
|
$filename = tempnam( get_temp_dir(), $theme_slug );
|
|
$zip = CBT_Theme_Zip::create_zip( $filename, $theme_slug );
|
|
|
|
$zip = CBT_Theme_Zip::copy_theme_to_zip( $zip, null, null );
|
|
|
|
if ( is_child_theme() ) {
|
|
wp_cache_flush();
|
|
$zip = CBT_Theme_Zip::add_templates_to_zip( $zip, 'current' );
|
|
$theme_json = CBT_Theme_JSON_Resolver::export_theme_data( 'current' );
|
|
} else {
|
|
$zip = CBT_Theme_Zip::add_templates_to_zip( $zip, 'all' );
|
|
$theme_json = CBT_Theme_JSON_Resolver::export_theme_data( 'all' );
|
|
}
|
|
|
|
$theme_json = CBT_Theme_Zip::add_activated_fonts_to_zip( $zip, $theme_json );
|
|
|
|
$zip = CBT_Theme_Zip::add_theme_json_to_zip( $zip, $theme_json );
|
|
|
|
$zip->close();
|
|
|
|
wp_cache_flush();
|
|
|
|
header( 'Content-Type: application/zip' );
|
|
header( 'Content-Disposition: attachment; filename=' . $theme_slug . '.zip' );
|
|
header( 'Content-Length: ' . filesize( $filename ) );
|
|
flush();
|
|
echo readfile( $filename );
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Update the theme metadata and relocate the theme.
|
|
*/
|
|
function rest_update_theme( $request ) {
|
|
$theme = $this->sanitize_theme_data( $request->get_params() );
|
|
|
|
// Update the metadata of the theme in the style.css file
|
|
$style_css = file_get_contents( get_stylesheet_directory() . '/style.css' );
|
|
$style_css = CBT_Theme_Styles::update_style_css( $style_css, $theme );
|
|
file_put_contents( get_stylesheet_directory() . '/style.css', $style_css );
|
|
|
|
file_put_contents(
|
|
CBT_Theme_Readme::file_path(),
|
|
CBT_Theme_Readme::update( $theme, CBT_Theme_Readme::get_content() )
|
|
);
|
|
|
|
// Replace Screenshot
|
|
if ( wp_get_theme()->get_screenshot() !== $theme['screenshot'] ) {
|
|
CBT_Theme_Utils::replace_screenshot( $theme['screenshot'] );
|
|
}
|
|
|
|
wp_cache_flush();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Theme Updated.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Save the user changes to the theme and clear user changes.
|
|
*/
|
|
function rest_save_theme( $request ) {
|
|
|
|
$options = $request->get_params();
|
|
|
|
if ( isset( $options['saveFonts'] ) && true === $options['saveFonts'] ) {
|
|
CBT_Theme_Fonts::persist_font_settings();
|
|
}
|
|
|
|
if ( isset( $options['saveTemplates'] ) && true === $options['saveTemplates'] ) {
|
|
if ( true === $options['processOnlySavedTemplates'] ) {
|
|
CBT_Theme_Templates::add_templates_to_local( 'user', null, null, $options );
|
|
} else {
|
|
if ( is_child_theme() ) {
|
|
CBT_Theme_Templates::add_templates_to_local( 'current', null, null, $options );
|
|
} else {
|
|
CBT_Theme_Templates::add_templates_to_local( 'all', null, null, $options );
|
|
}
|
|
}
|
|
CBT_Theme_Templates::clear_user_templates_customizations();
|
|
CBT_Theme_Templates::clear_user_template_parts_customizations();
|
|
}
|
|
|
|
if ( isset( $options['saveStyle'] ) && true === $options['saveStyle'] ) {
|
|
if ( is_child_theme() ) {
|
|
CBT_Theme_JSON::add_theme_json_to_local( 'current', null, null, $options );
|
|
} else {
|
|
CBT_Theme_JSON::add_theme_json_to_local( 'all', null, null, $options );
|
|
}
|
|
CBT_Theme_Styles::clear_user_styles_customizations();
|
|
}
|
|
|
|
if ( isset( $options['savePatterns'] ) && true === $options['savePatterns'] ) {
|
|
$response = CBT_Theme_Patterns::add_patterns_to_theme( $options );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
wp_get_theme()->cache_delete();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Theme Saved.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Persist a partial theme.json payload from the Edit Theme Settings modal.
|
|
*
|
|
* Accepts the keys `settings`, `customTemplates`, `templateParts`, and
|
|
* `removedShadowDefaults`. Only keys present in the payload are written;
|
|
* missing keys leave the existing theme.json untouched.
|
|
*/
|
|
function rest_save_theme_settings( $request ) {
|
|
// `get_json_params()` returns null (or a scalar) for empty or
|
|
// non-object request bodies. The service signature requires an array,
|
|
// so guard here and return a 400 rather than letting the type hint
|
|
// fatal at the service boundary.
|
|
$payload = $request->get_json_params();
|
|
if ( ! is_array( $payload ) ) {
|
|
return new WP_Error(
|
|
'cbt_invalid_payload',
|
|
__( 'Request body must be a JSON object.', 'create-block-theme' ),
|
|
array( 'status' => 400 )
|
|
);
|
|
}
|
|
|
|
$result = CBT_Theme_Settings_Save::run( $payload );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
return $result;
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'theme_json' => $result,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a list of all the font families used in the theme.
|
|
*
|
|
* It includes the font families from the theme.json data (theme.json file + global styles) and the theme style variations.
|
|
* The font families with font faces containing src urls relative to the theme folder are converted to absolute urls.
|
|
*/
|
|
function rest_get_font_families() {
|
|
$font_families = CBT_Theme_Fonts::get_all_fonts();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Font Families retrieved.', 'create-block-theme' ),
|
|
'data' => $font_families,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reset the theme to the default state.
|
|
*/
|
|
function rest_reset_theme( $request ) {
|
|
$options = $request->get_params();
|
|
|
|
if ( isset( $options['resetStyles'] ) && true === $options['resetStyles'] ) {
|
|
CBT_Theme_Styles::clear_user_styles_customizations();
|
|
}
|
|
|
|
if ( isset( $options['resetTemplates'] ) && true === $options['resetTemplates'] ) {
|
|
CBT_Theme_Templates::clear_user_templates_customizations();
|
|
}
|
|
|
|
if ( isset( $options['resetTemplateParts'] ) && true === $options['resetTemplateParts'] ) {
|
|
CBT_Theme_Templates::clear_user_template_parts_customizations();
|
|
}
|
|
|
|
return rest_ensure_response(
|
|
array(
|
|
'status' => 'SUCCESS',
|
|
'message' => __( 'Theme Reset.', 'create-block-theme' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
private function sanitize_theme_data( $theme ) {
|
|
$sanitized_theme = array();
|
|
$sanitized_theme['name'] = sanitize_text_field( $theme['name'] );
|
|
$sanitized_theme['description'] = sanitize_text_field( $theme['description'] ?? '' );
|
|
$sanitized_theme['uri'] = sanitize_text_field( $theme['uri'] ?? '' );
|
|
$sanitized_theme['author'] = sanitize_text_field( $theme['author'] ?? '' );
|
|
$sanitized_theme['author_uri'] = sanitize_text_field( $theme['author_uri'] ?? '' );
|
|
$sanitized_theme['tags_custom'] = sanitize_text_field( $theme['tags_custom'] ?? '' );
|
|
$sanitized_theme['version'] = sanitize_text_field( $theme['version'] ?? '' );
|
|
$sanitized_theme['screenshot'] = sanitize_text_field( $theme['screenshot'] ?? '' );
|
|
$sanitized_theme['requires_wp'] = sanitize_text_field( $theme['requires_wp'] ?? '' );
|
|
$sanitized_theme['recommended_plugins'] = sanitize_textarea_field( $theme['recommended_plugins'] ?? '' );
|
|
$sanitized_theme['font_credits'] = sanitize_textarea_field( $theme['font_credits'] ?? '' );
|
|
$sanitized_theme['image_credits'] = sanitize_textarea_field( $theme['image_credits'] ?? '' );
|
|
$sanitized_theme['template'] = '';
|
|
$sanitized_theme['slug'] = sanitize_title( $theme['name'] );
|
|
$sanitized_theme['text_domain'] = $sanitized_theme['slug'];
|
|
return $sanitized_theme;
|
|
}
|
|
|
|
/**
|
|
* Permission check for filesystem-mutating REST routes.
|
|
*
|
|
* Combines two WordPress Core primitives:
|
|
*
|
|
* - `current_user_can( 'edit_themes' )` — Core's canonical theme-file
|
|
* capability. Held by Administrators on single-site, super-admins on
|
|
* multisite (NOT sub-site admins), and automatically denied when
|
|
* `DISALLOW_FILE_EDIT` is defined. This single check covers the
|
|
* multisite tenant boundary and the `DISALLOW_FILE_EDIT` hardening
|
|
* that the plugin honoured pre-v2.1.2.
|
|
* - `wp_is_file_mod_allowed( 'create_block_theme_modify_theme' )` —
|
|
* Core's canonical file-modification gate. Handles `DISALLOW_FILE_MODS`
|
|
* and the `file_mod_allowed` filter (used by hosts / security plugins).
|
|
*
|
|
* @return bool True when both checks pass.
|
|
*/
|
|
public static function can_modify_theme() {
|
|
return current_user_can( 'edit_themes' )
|
|
&& wp_is_file_mod_allowed( 'create_block_theme_modify_theme' );
|
|
}
|
|
}
|