@@ -124,6 +124,153 @@ 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 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+ list ( $ token_type , , $ attrs , $ start_offset , $ token_length ) = $ next_token ;
156+
157+ if ( 'no-more-tokens ' === $ token_type ) {
158+ break ;
159+ }
160+
161+ $ parser ->offset = $ start_offset + $ token_length ;
162+
163+ if ( 'block-opener ' !== $ token_type && 'void-block ' !== $ token_type ) {
164+ continue ;
165+ }
166+
167+ if ( ! isset ( $ attrs ['style ' ]['css ' ] ) ) {
168+ continue ;
169+ }
170+
171+ // Remove css and clean up empty style.
172+ unset( $ attrs ['style ' ]['css ' ] );
173+ if ( empty ( $ attrs ['style ' ] ) ) {
174+ unset( $ attrs ['style ' ] );
175+ }
176+
177+ // Locate the JSON portion within the token.
178+ $ token_string = substr ( $ unslashed , $ start_offset , $ token_length );
179+ $ json_rel_start = strcspn ( $ token_string , '{ ' );
180+ $ json_rel_end = strrpos ( $ token_string , '} ' );
181+
182+ $ json_start = $ start_offset + $ json_rel_start ;
183+ $ json_length = $ json_rel_end - $ json_rel_start + 1 ;
184+
185+ // Re-encode attributes. If attrs is now empty, remove JSON and trailing space.
186+ if ( empty ( $ attrs ) ) {
187+ // Remove the trailing space after JSON.
188+ $ replacements [] = array ( $ json_start , $ json_length + 1 , '' );
189+ } else {
190+ $ replacements [] = array ( $ json_start , $ json_length , serialize_block_attributes ( $ attrs ) );
191+ }
192+ }
193+
194+ if ( empty ( $ replacements ) ) {
195+ return $ content ;
196+ }
197+
198+ // Build the result by splicing replacements into the original string.
199+ $ result = '' ;
200+ $ was_at = 0 ;
201+
202+ foreach ( $ replacements as $ replacement ) {
203+ list ( $ offset , $ length , $ new_json ) = $ replacement ;
204+ $ result .= substr ( $ unslashed , $ was_at , $ offset - $ was_at ) . $ new_json ;
205+ $ was_at = $ offset + $ length ;
206+ }
207+
208+ if ( $ was_at < $ end ) {
209+ $ result .= substr ( $ unslashed , $ was_at );
210+ }
211+
212+ return addslashes ( $ result );
213+ }
214+
215+ /**
216+ * Adds the filters to strip custom CSS from block content on save.
217+ *
218+ * @since 7.0.0
219+ * @access private
220+ */
221+ function wp_custom_css_kses_init_filters () {
222+ add_filter ( 'content_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
223+ add_filter ( 'content_filtered_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
224+ }
225+
226+ /**
227+ * Removes the filters that strip custom CSS from block content on save.
228+ *
229+ * @since 7.0.0
230+ * @access private
231+ */
232+ function wp_custom_css_remove_filters () {
233+ remove_filter ( 'content_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
234+ remove_filter ( 'content_filtered_save_pre ' , 'wp_strip_custom_css_from_blocks ' , 8 );
235+ }
236+
237+ /**
238+ * Registers the custom CSS content filters if the user does not have the edit_css capability.
239+ *
240+ * @since 7.0.0
241+ * @access private
242+ */
243+ function wp_custom_css_kses_init () {
244+ wp_custom_css_remove_filters ();
245+ if ( ! current_user_can ( 'edit_css ' ) ) {
246+ wp_custom_css_kses_init_filters ();
247+ }
248+ }
249+
250+ /**
251+ * Initializes custom CSS content filters when imported data should be filtered.
252+ *
253+ * This filter is the last being executed on force_filtered_html_on_import.
254+ * If the input of the filter is true it means we are in an import situation and should
255+ * enable the custom CSS filters, independently of the user capabilities.
256+ *
257+ * @since 7.0.0
258+ * @access private
259+ *
260+ * @param mixed $arg Input argument of the filter.
261+ * @return mixed Input argument of the filter.
262+ */
263+ function wp_custom_css_force_filtered_html_on_import_filter ( $ arg ) {
264+ if ( $ arg ) {
265+ wp_custom_css_kses_init_filters ();
266+ }
267+ return $ arg ;
268+ }
269+
270+ add_action ( 'init ' , 'wp_custom_css_kses_init ' , 20 );
271+ add_action ( 'set_current_user ' , 'wp_custom_css_kses_init ' );
272+ add_filter ( 'force_filtered_html_on_import ' , 'wp_custom_css_force_filtered_html_on_import_filter ' , 999 );
273+
127274// Register the block support.
128275WP_Block_Supports::get_instance ()->register (
129276 'custom-css ' ,
0 commit comments