@@ -124,6 +124,158 @@ function wp_register_custom_css_support( $block_type ) {
124124 }
125125}
126126
127+ /**
128+ * Strips `style.css` attributes from all blocks in post content.
129+ *
130+ * Uses {@see WP_Block_Parser::next_token()} to scan block tokens and surgically
131+ * replace only the attribute JSON that changed — no parse_blocks() +
132+ * serialize_blocks() round-trip needed.
133+ *
134+ * @since 7.0.0
135+ * @access private
136+ *
137+ * @param string $content Post content to filter, expected to be escaped with slashes.
138+ * @return string Filtered post content with block custom CSS removed.
139+ */
140+ function wp_strip_custom_css_from_blocks ( $ content ) {
141+ if ( ! has_blocks ( $ content ) ) {
142+ return $ content ;
143+ }
144+
145+ $ unslashed = stripslashes ( $ content );
146+
147+ $ parser = new WP_Block_Parser ();
148+ $ parser ->document = $ unslashed ;
149+ $ parser ->offset = 0 ;
150+ $ end = strlen ( $ unslashed );
151+ $ replacements = array ();
152+
153+ while ( $ parser ->offset < $ end ) {
154+ $ next_token = $ parser ->next_token ();
155+
156+ if ( 'no-more-tokens ' === $ next_token [0 ] ) {
157+ break ;
158+ }
159+
160+ list ( $ token_type , , $ attrs , $ start_offset , $ token_length ) = $ next_token ;
161+
162+ $ parser ->offset = $ start_offset + $ token_length ;
163+
164+ if ( 'block-opener ' !== $ token_type && 'void-block ' !== $ token_type ) {
165+ continue ;
166+ }
167+
168+ if ( ! isset ( $ attrs ['style ' ]['css ' ] ) ) {
169+ continue ;
170+ }
171+
172+ // Remove css and clean up empty style.
173+ unset( $ attrs ['style ' ]['css ' ] );
174+ if ( empty ( $ attrs ['style ' ] ) ) {
175+ unset( $ attrs ['style ' ] );
176+ }
177+
178+ // Locate the JSON portion within the token.
179+ $ token_string = substr ( $ unslashed , $ start_offset , $ token_length );
180+ $ json_rel_start = strcspn ( $ token_string , '{ ' );
181+ $ json_rel_end = strrpos ( $ token_string , '} ' );
182+
183+ $ json_start = $ start_offset + $ json_rel_start ;
184+ $ json_length = $ json_rel_end - $ json_rel_start + 1 ;
185+
186+ // Re-encode attributes. If attrs is now empty, remove JSON and trailing space.
187+ if ( empty ( $ attrs ) ) {
188+ // Remove the trailing space after JSON.
189+ $ replacements [] = array ( $ json_start , $ json_length + 1 , '' );
190+ } else {
191+ $ replacements [] = array ( $ json_start , $ json_length , serialize_block_attributes ( $ attrs ) );
192+ }
193+ }
194+
195+ if ( empty ( $ replacements ) ) {
196+ return $ content ;
197+ }
198+
199+ // Build the result by splicing replacements into the original string.
200+ $ result = '' ;
201+ $ was_at = 0 ;
202+
203+ foreach ( $ replacements as $ replacement ) {
204+ list ( $ offset , $ length , $ new_json ) = $ replacement ;
205+ $ result .= substr ( $ unslashed , $ was_at , $ offset - $ was_at ) . $ new_json ;
206+ $ was_at = $ offset + $ length ;
207+ }
208+
209+ if ( $ was_at < $ end ) {
210+ $ result .= substr ( $ unslashed , $ was_at );
211+ }
212+
213+ return addslashes ( $ result );
214+ }
215+
216+ /**
217+ * Adds the filters to strip custom CSS from block content on save.
218+ * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10).
219+ *
220+ * @since 7.0.0
221+ * @access private
222+ */
223+ function wp_custom_css_kses_init_filters () {
224+ add_filter ( 'content_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
225+ add_filter ( 'content_filtered_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
226+ }
227+
228+ /**
229+ * Removes the filters that strip custom CSS from block content on save.
230+ * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10).
231+ *
232+ * @since 7.0.0
233+ * @access private
234+ */
235+ function wp_custom_css_remove_filters () {
236+ remove_filter ( 'content_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
237+ remove_filter ( 'content_filtered_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
238+ }
239+
240+ /**
241+ * Registers the custom CSS content filters if the user does not have the edit_css capability.
242+ *
243+ * @since 7.0.0
244+ * @access private
245+ */
246+ function wp_custom_css_kses_init () {
247+ wp_custom_css_remove_filters ();
248+ if ( ! current_user_can ( 'edit_css ' ) ) {
249+ wp_custom_css_kses_init_filters ();
250+ }
251+ }
252+
253+ /**
254+ * Initializes custom CSS content filters when imported data should be filtered.
255+ *
256+ * Runs at priority 999 on {@see 'force_filtered_html_on_import'} to ensure it
257+ * fires after general KSES initialization, independently of user capabilities.
258+ * If the input of the filter is true it means we are in an import situation and should
259+ * enable the custom CSS filters, independently of the user capabilities.
260+ *
261+ * @since 7.0.0
262+ * @access private
263+ *
264+ * @param mixed $arg Input argument of the filter.
265+ * @return mixed Input argument of the filter.
266+ */
267+ function wp_custom_css_force_filtered_html_on_import_filter ( $ arg ) {
268+ if ( $ arg ) {
269+ wp_custom_css_kses_init_filters ();
270+ }
271+ return $ arg ;
272+ }
273+
274+ // Run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10).
275+ add_action ( 'init ' , 'wp_custom_css_kses_init ' , 20 );
276+ add_action ( 'set_current_user ' , 'wp_custom_css_kses_init ' );
277+ add_filter ( 'force_filtered_html_on_import ' , 'wp_custom_css_force_filtered_html_on_import_filter ' , 999 );
278+
127279// Register the block support.
128280WP_Block_Supports::get_instance ()->register (
129281 'custom-css ' ,
0 commit comments