-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Block Editor: Strip per-block custom CSS on save for users without edit_css #76650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b21a33f
a3cf6a7
5a14204
e8f7969
6f058a4
0385bd1
02ab72b
519079a
0611a14
9eb0d97
13f183b
ab4a981
f9947cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| https://github.com/WordPress/wordpress-develop/pull/11347 | ||
|
|
||
| * https://github.com/WordPress/gutenberg/pull/76650 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,8 +8,6 @@ | |
| /** | ||
| * Render the custom CSS stylesheet and add class name to block as required. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param array $parsed_block The parsed block. | ||
| * @return array The same parsed block with custom CSS class name added if appropriate. | ||
| */ | ||
|
|
@@ -58,8 +56,6 @@ function gutenberg_render_custom_css_support_styles( $parsed_block ) { | |
|
|
||
| /** | ||
| * Enqueues the block custom CSS styles. | ||
| * | ||
| * @since 7.0.0 | ||
| */ | ||
| function gutenberg_enqueue_block_custom_css() { | ||
| wp_enqueue_style( 'wp-block-custom-css' ); | ||
|
|
@@ -71,8 +67,6 @@ function gutenberg_enqueue_block_custom_css() { | |
| * The class name is generated in `gutenberg_render_custom_css_support_styles` | ||
| * and stored in block attributes. This filter adds it to the actual markup. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $block_content Rendered block content. | ||
| * @param array $block Block object. | ||
| * @return string Filtered block content. | ||
|
|
@@ -136,6 +130,142 @@ function gutenberg_register_custom_css_support( $block_type ) { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Strips `style.css` attributes from all blocks in post content. | ||
| * | ||
| * Uses WP_Block_Parser::next_token() to scan block tokens and surgically | ||
| * replace only the attribute JSON that changed — no parse_blocks() + | ||
| * serialize_blocks() round-trip needed. | ||
| * | ||
| * @param string $content Post content to filter, expected to be escaped with slashes. | ||
| * @return string Filtered post content with block custom CSS removed. | ||
| */ | ||
| function gutenberg_strip_custom_css_from_blocks( $content ) { | ||
| if ( ! has_blocks( $content ) ) { | ||
| return $content; | ||
| } | ||
|
|
||
| $unslashed = stripslashes( $content ); | ||
|
|
||
| $parser = new WP_Block_Parser(); | ||
| $parser->document = $unslashed; | ||
| $parser->offset = 0; | ||
| $end = strlen( $unslashed ); | ||
| $replacements = array(); | ||
|
|
||
| while ( $parser->offset < $end ) { | ||
| $next_token = $parser->next_token(); | ||
| list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token; | ||
|
|
||
| if ( 'no-more-tokens' === $token_type ) { | ||
| break; | ||
| } | ||
|
|
||
| $parser->offset = $start_offset + $token_length; | ||
|
|
||
| if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) { | ||
| continue; | ||
| } | ||
|
|
||
| if ( ! isset( $attrs['style']['css'] ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| // Remove css and clean up empty style. | ||
| unset( $attrs['style']['css'] ); | ||
| if ( empty( $attrs['style'] ) ) { | ||
| unset( $attrs['style'] ); | ||
| } | ||
|
|
||
| // Locate the JSON portion within the token. | ||
| $token_string = substr( $unslashed, $start_offset, $token_length ); | ||
| $json_rel_start = strcspn( $token_string, '{' ); | ||
| $json_rel_end = strrpos( $token_string, '}' ); | ||
|
|
||
| $json_start = $start_offset + $json_rel_start; | ||
| $json_length = $json_rel_end - $json_rel_start + 1; | ||
|
|
||
| // Re-encode attributes. If attrs is now empty, remove JSON and trailing space. | ||
| if ( empty( $attrs ) ) { | ||
| // Remove the trailing space after JSON: `{"style":{"css":"x"}} ` → `` | ||
| $replacements[] = array( $json_start, $json_length + 1, '' ); | ||
| } else { | ||
| $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) ); | ||
| } | ||
| } | ||
|
|
||
| if ( empty( $replacements ) ) { | ||
| return $content; | ||
| } | ||
|
|
||
| // Build the result by splicing replacements into the original string. | ||
| $result = ''; | ||
| $was_at = 0; | ||
|
|
||
| foreach ( $replacements as $replacement ) { | ||
| list( $offset, $length, $new_json ) = $replacement; | ||
| $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json; | ||
| $was_at = $offset + $length; | ||
| } | ||
|
|
||
| if ( $was_at < $end ) { | ||
| $result .= substr( $unslashed, $was_at ); | ||
| } | ||
|
|
||
| return addslashes( $result ); | ||
| } | ||
|
|
||
| /** | ||
| * Adds the filters to strip custom CSS from block content on save. | ||
| * @access private | ||
| */ | ||
| function gutenberg_custom_css_kses_init_filters() { | ||
| add_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 ); | ||
| add_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 ); | ||
|
Comment on lines
+223
to
+224
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wp_insert_post_data looks like it comes bundled with It runs after KSES, but I guess that doesn't matter if we're stripping it. And I think you wouldn't need the I could be missing something obvious.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a fair point —
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have a strong opinion, and thanks for confirming. As noted above I'm just thinking about how this would look in Core, and where the processing would take place. I guess it's contained to the https://github.com/wordpress/wordpress-develop/blob/trunk/src/wp-includes/blocks.php#L3107 |
||
| } | ||
|
|
||
| /** | ||
| * Removes the filters that strip custom CSS from block content on save. | ||
| * @access private | ||
| */ | ||
| function gutenberg_custom_css_remove_filters() { | ||
| remove_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 ); | ||
| remove_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the custom CSS content filters if the user does not have the edit_css capability. | ||
| * @access private | ||
| */ | ||
| function gutenberg_custom_css_kses_init() { | ||
| gutenberg_custom_css_remove_filters(); | ||
| if ( ! current_user_can( 'edit_css' ) ) { | ||
|
ramonjd marked this conversation as resolved.
|
||
| gutenberg_custom_css_kses_init_filters(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Initializes custom CSS content filters when imported data should be filtered. | ||
| * | ||
| * This filter is the last being executed on force_filtered_html_on_import. | ||
| * If the input of the filter is true it means we are in an import situation and should | ||
| * enable the custom CSS filters, independently of the user capabilities. | ||
| * @access private | ||
| * | ||
| * @param mixed $arg Input argument of the filter. | ||
| * @return mixed Input argument of the filter. | ||
| */ | ||
| function gutenberg_custom_css_force_filtered_html_on_import_filter( $arg ) { | ||
| if ( $arg ) { | ||
| gutenberg_custom_css_kses_init_filters(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this doing exactly? Would it strip CSS for admins during import as well?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is the import safety net — again mirroring what Core does for KSES. When
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, thanks for the explainer |
||
| } | ||
| return $arg; | ||
| } | ||
|
|
||
| add_action( 'init', 'gutenberg_custom_css_kses_init', 20 ); | ||
| add_action( 'set_current_user', 'gutenberg_custom_css_kses_init' ); | ||
|
Comment on lines
+265
to
+266
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just so I understand, these checks remove/add the hooks for different scenarios, e.g., page requests and REST?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes exactly. This mirrors the pattern Core uses for KSES in |
||
| add_filter( 'force_filtered_html_on_import', 'gutenberg_custom_css_force_filtered_html_on_import_filter', 999 ); | ||
|
|
||
| // Register the block support. | ||
| WP_Block_Supports::get_instance()->register( | ||
| 'custom-css', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are your thoughts on the migration path to Core here? I hesitate to say it out loud but
filter_block_kses_valueseems like candidate. Anyway, if and when it becomes clear, it might be worth noting them for the backport.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question.
filter_block_kses_valueis an interesting candidate — it already walks block attributes during KSES and could potentially stripstyle.cssinline rather than needing the separate content filter approach. The tricky part is thatfilter_block_kses_valueoperates at the attribute level during KSES processing, while this approach operates on the full serialized content, and probably more importantly it was already noted on the abandoned fix in core that this is not a KSES issue - mixing the CSS fix back into KSES at this point seems to muddy that water again.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's tricky 🤔
The question is going to come up at some point though: "Where would this all live in Core?" As a set of filters perhaps?