PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 All 202 releases
← All changes | includes/Modules/StyleHandler.php +292 -96 4.5.4 → 4.9.4 View file →
@@ -1,8 +1,20 @@
1 1 <?php
2 +namespace WPDeveloper\BetterDocs\Modules;
2 3
3 -namespace WPDeveloper\BetterDocs\Modules;
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
4 7
8 +
9 +
10 +// Doc/category-listing primitives (meta_query/tax_query, post__not_in/exclude)
11 +// are intrinsic to BetterDocs' KB / category / FAQ filters and intentional.
12 +// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
13 +// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
14 +// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
15 +// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
16 +// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
5 17 use WPDeveloper\BetterDocs\Utils\CSSParser;
6 18
7 19 final class StyleHandler {
8 20 private static $instance;
@@ -42,9 +54,19 @@
42 54
43 55 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ), 99 );
44 56 add_action( 'save_post', array( $this, 'on_save_post' ), 10, 3 );
45 57 add_action( 'wp', array( $this, 'generate_post_content' ) );
58 +
59 + // Synced patterns in block widgets render on every page; track them
60 + // once the widget/sidebar changes of the request are all saved.
61 + add_action( 'rest_after_save_widget', array( $this, 'schedule_widget_patterns_refresh' ) );
62 + add_action( 'rest_delete_widget', array( $this, 'schedule_widget_patterns_refresh' ) );
63 + add_action( 'rest_save_sidebar', array( $this, 'schedule_widget_patterns_refresh' ) );
64 + add_action( 'after_switch_theme', array( $this, 'schedule_widget_patterns_refresh' ) );
46 65 add_action( 'rest_after_save_widget', array( $this, 'after_save_widget' ), 10, 4 );
66 + add_action( 'rest_delete_widget', array( $this, 'after_save_widget' ) );
67 + add_action( 'rest_save_sidebar', array( $this, 'after_save_widget' ) );
68 + add_action( 'after_switch_theme', array( $this, 'after_save_widget' ) );
47 69
48 70 // FSE assets generation
49 71 add_action(
50 72 'init',
@@ -113,12 +135,12 @@
113 135 */
114 136 public function write_css_from_content( $post, $post_id, $parsed_content ) {
115 137 $betterdocs_blocks = array( );
116 138 $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
117 - $reusable_Blocks = ! empty( $recursive_response[ 'reusableBlocks' ] ) ? $recursive_response[ 'reusableBlocks' ] : array( );
139 + $all_reusable_blocks = ! empty( $recursive_response[ 'reusableBlocks' ] ) ? $recursive_response[ 'reusableBlocks' ] : array( );
118 140 // remove empty reusable blocks
119 141 $reusable_Blocks = array_filter(
120 - $reusable_Blocks,
142 + $all_reusable_blocks,
121 143 function ( $v ) {
122 144 return ! empty( $v );
123 145 }
124 146 );
@@ -124,40 +146,111 @@
124 146 );
125 147 unset( $recursive_response[ 'reusableBlocks' ] );
126 148 $style = CSSParser::blocks_to_style_array( $recursive_response );
127 149 $reusableIds = $reusable_Blocks ? array_keys( $reusable_Blocks ) : array( );
128 - if ( ! empty( $reusableIds ) ) {
150 + // The option is merged into every page's enqueue, so only FSE
151 + // templates / template parts (rendered around every page) may set
152 + // it. Ordinary posts keep their ids in post meta below; they used to
153 + // overwrite the option too, so whatever the last post with synced
154 + // patterns referenced was loaded site-wide.
155 + if ( ! empty( $reusableIds ) && $this->is_fse_template( $post ) ) {
129 156 update_option( '_betterdocs_reusable_block_ids', $reusableIds );
130 157 }
131 158 update_post_meta( $post_id, '_betterdocs_reusable_block_ids', $reusableIds );
132 159 $this->write_block_css( $style, $post ); //Write CSS file for this page
133 160
134 - if ( ! empty( $reusable_Blocks ) ) {
135 - foreach ( $reusable_Blocks as $blockId => $block ) {
136 - $style = CSSParser::blocks_to_style_array( $block );
137 - $this->write_reusable_block_css( $style, $blockId );
138 - }
161 + // Every referenced reusable block, including those with no BetterDocs
162 + // styles, so their stale files (e.g. pre-#174 copies of Essential
163 + // Blocks' CSS) are removed.
164 + foreach ( $all_reusable_blocks as $blockId => $block ) {
165 + $style = CSSParser::blocks_to_style_array( $block );
166 + $this->write_reusable_block_css( $style, $blockId );
139 167 }
140 168 }
141 169
142 170 /**
143 - * Save Widget CSS when Widget is saved
171 + * Whether a post is an FSE template or template part.
172 + *
173 + * @param \WP_Post|null $post Post.
174 + * @return bool
175 + */
176 + private function is_fse_template( $post ) {
177 + return isset( $post->post_type ) && ( 'wp_template_part' === $post->post_type || 'wp_template' === $post->post_type );
178 + }
179 +
180 + /**
181 + * Rebuild the widget CSS when a widget or sidebar is saved or a widget is
182 + * deleted — once, at the end of the request: core fires
183 + * rest_after_save_widget before it assigns the widget to its sidebar.
144 184 * @return void
145 185 * @since 3.5.3
146 186 */
147 - public function after_save_widget( $id, $sidebar_id, $request, $creating ) {
148 - $parsed_content = isset( $request[ 'instance' ][ 'raw' ][ 'content' ] ) ? parse_blocks( $request[ 'instance' ][ 'raw' ][ 'content' ] ) : array( );
149 - if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
187 + public function after_save_widget() {
188 + if ( false === has_action( 'shutdown', array( $this, 'rebuild_widget_css' ) ) ) {
189 + add_action( 'shutdown', array( $this, 'rebuild_widget_css' ) );
190 + }
191 + }
192 +
193 + /**
194 + * Write the widget CSS file from every active block widget's BetterDocs
195 + * blocks, or remove it when there are none. It used to hold only the
196 + * widget saved last, and was never removed (e.g. stale Essential Blocks
197 + * CSS collected before #174 stayed loaded on every page).
198 + * @return void
199 + */
200 + public function rebuild_widget_css() {
201 + $styles = array( );
202 + foreach ( $this->active_block_widget_contents() as $content ) {
203 + if ( false === strpos( $content, '<!-- wp:betterdocs/' ) ) {
204 + continue;
205 + }
150 206 $betterdocs_blocks = array( );
151 - $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
207 + $recursive_response = CSSParser::betterdocs_block_style_recursive( parse_blocks( $content ), $betterdocs_blocks );
152 208 unset( $recursive_response[ 'reusableBlocks' ] );
153 - $style = CSSParser::blocks_to_style_array( $recursive_response );
154 - //Write CSS file for Widget
155 - $this->single_file_css_generator( $style, $this->style_dir, $this->prefix . '-widget.min.css' );
209 + $styles = array_merge( $styles, CSSParser::blocks_to_style_array( $recursive_response ) );
156 210 }
211 +
212 + $file = $this->style_dir . $this->prefix . '-widget.min.css';
213 + $css = empty( $styles ) ? '' : CSSParser::build_css( $styles );
214 + if ( empty( $css ) ) {
215 + if ( file_exists( $file ) ) {
216 + wp_delete_file( $file );
217 + }
218 + return;
219 + }
220 +
221 + // Unchanged CSS: leave the file (and its mtime-based version) alone.
222 + if ( file_exists( $file ) && file_get_contents( $file ) === $css ) {
223 + return;
224 + }
225 +
226 + if ( ! file_exists( $this->style_dir ) ) {
227 + wp_mkdir_p( $this->style_dir );
228 + }
229 + file_put_contents( $file, $css );
157 230 }
158 231
159 232 /**
233 + * Content of the block widgets placed in a sidebar (not inactive).
234 + * @return string[]
235 + */
236 + private function active_block_widget_contents() {
237 + $contents = array( );
238 + $instances = get_option( 'widget_block', array( ) );
239 + foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
240 + if ( 'wp_inactive_widgets' === $sidebar_id || ! is_array( $widget_ids ) ) {
241 + continue;
242 + }
243 + foreach ( $widget_ids as $widget_id ) {
244 + if ( preg_match( '/^block-(\d+)$/', (string) $widget_id, $matches ) && ! empty( $instances[ $matches[1] ]['content'] ) ) {
245 + $contents[] = (string) $instances[ $matches[1] ]['content'];
246 + }
247 + }
248 + }
249 + return $contents;
250 + }
251 +
252 + /**
160 253 * Load Dependencies
161 254 */
162 255 private function load_style_handler_dependencies() {
163 256 require_once plugin_dir_path( __FILE__ ) . 'includes/class-parse-css.php';
@@ -172,14 +265,15 @@
172 265 global $post;
173 266 $deps = apply_filters( 'betterdocs_generated_css_frontend_deps', array( ) );
174 267
175 268 // generatepress elements
269 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WP core 'active_plugins' filter, applied to detect GeneratePress Premium presence on multisite.
176 270 if ( in_array( 'gp-premium/gp-premium.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
177 271 $gp_elements = get_posts( array( 'post_type' => 'gp_elements' ) );
178 272 if ( is_array( $gp_elements ) && ! empty( $gp_elements ) ) {
179 273 foreach ( $gp_elements as $element ) {
180 274 if ( file_exists( $this->style_dir . $this->prefix . '-' . $element->ID . '.min.css' ) ) {
181 - wp_enqueue_style( 'betterdocs-block-style-' . $element->ID, $this->style_url . $this->prefix . '-' . $element->ID . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
275 + wp_enqueue_style( 'betterdocs-block-style-' . $element->ID, $this->style_url . $this->prefix . '-' . $element->ID . '.min.css', $deps, $this->file_version( $this->style_dir . $this->prefix . '-' . $element->ID . '.min.css' ) );
182 276 }
183 277 }
184 278 }
185 279 }
@@ -184,11 +278,16 @@
184 278 }
185 279 }
186 280
187 281 if ( ! empty( $post ) && ! empty( $post->ID ) ) {
188 - //Page/Post Style Enqueue
189 - if ( file_exists( $this->style_dir . $this->prefix . '-' . $post->ID . '.min.css' ) ) {
190 - wp_enqueue_style( 'betterdocs-block-style-' . $post->ID, $this->style_url . $this->prefix . '-' . $post->ID . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
282 + //Page/Post Style Enqueue — only for posts that hold BetterDocs
283 + // blocks. Until #174 the parser also collected Essential Blocks'
284 + // blocks, so files written then for pages without BetterDocs
285 + // blocks are stale duplicates of Essential Blocks' CSS; they are
286 + // skipped here and removed on the post's next save (#174).
287 + $post_css = $this->style_dir . $this->prefix . '-' . $post->ID . '.min.css';
288 + if ( file_exists( $post_css ) && false !== strpos( (string) $post->post_content, '<!-- wp:betterdocs/' ) ) {
289 + wp_enqueue_style( 'betterdocs-block-style-' . $post->ID, $this->style_url . $this->prefix . '-' . $post->ID . '.min.css', $deps, $this->file_version( $post_css ) );
191 290 }
192 291
193 292 // Reusable block Style Enqueues
194 293 $reusableIds = get_post_meta( $post->ID, '_betterdocs_reusable_block_ids', true );
@@ -194,22 +293,30 @@
194 293 $reusableIds = get_post_meta( $post->ID, '_betterdocs_reusable_block_ids', true );
195 294 $reusableIds = ! empty( $reusableIds ) ? $reusableIds : array( );
196 295 $templateReusableIds = get_option( '_betterdocs_reusable_block_ids', array( ) );
197 296 $reusableIds = array_unique( array_merge( $reusableIds, $templateReusableIds ) );
198 - if ( ! empty( $reusableIds ) ) {
199 - foreach ( $reusableIds as $reusableId ) {
200 - if ( file_exists( $this->style_dir . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css' ) ) {
201 - wp_enqueue_style( 'betterdocs-reusable-block-style-' . $reusableId, $this->style_url . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
202 - }
203 - }
297 + foreach ( $reusableIds as $reusableId ) {
298 + $this->enqueue_reusable_block_style( $reusableId, $deps );
204 299 }
205 300 }
206 301
207 - //Widget Style Enqueue
208 - if ( file_exists( $this->style_dir . $this->prefix . '-widget.min.css' ) ) {
209 - wp_enqueue_style( 'betterdocs-widget-style', $this->style_url . $this->prefix . '-widget.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
302 + //Widget Style Enqueue — only while an active block widget holds
303 + // BetterDocs blocks, so a stale file (see rebuild_widget_css()) is
304 + // not loaded site-wide until the next widget save removes it.
305 + if ( file_exists( $this->style_dir . $this->prefix . '-widget.min.css' ) && false !== strpos( implode( '', $this->active_block_widget_contents() ), '<!-- wp:betterdocs/' ) ) {
306 + wp_enqueue_style( 'betterdocs-widget-style', $this->style_url . $this->prefix . '-widget.min.css', $deps, $this->file_version( $this->style_dir . $this->prefix . '-widget.min.css' ) );
210 307 }
211 308
309 + // Synced patterns in block widgets, on every page. Built once on
310 + // sites whose widgets were saved before this list existed.
311 + $widgetReusableIds = get_option( '_betterdocs_widget_reusable_block_ids', null );
312 + if ( null === $widgetReusableIds ) {
313 + $widgetReusableIds = $this->refresh_widget_patterns();
314 + }
315 + foreach ( (array) $widgetReusableIds as $reusableId ) {
316 + $this->enqueue_reusable_block_style( $reusableId, $deps );
317 + }
318 +
212 319 //FSE Style Enqueue — one file per FSE template/template-part post so
213 320 // styles do not bleed across templates (issue #53).
214 321 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && betterdocs()->helper->is_templates() && ! empty( self::$_fse_template_post_ids ) ) {
215 322 foreach ( array_unique( self::$_fse_template_post_ids ) as $template_post_id ) {
@@ -218,9 +325,9 @@
218 325 wp_enqueue_style(
219 326 'betterdocs-fullsite-style-' . abs( $template_post_id ),
220 327 $this->style_url . $file_name,
221 328 $deps,
222 - substr( md5( microtime( true ) ), 0, 10 )
329 + $this->file_version( $this->style_dir . $file_name )
223 330 );
224 331 }
225 332 }
226 333 }
@@ -258,8 +365,18 @@
258 365 }
259 366
260 367 $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
261 368
369 + // A synced pattern is styled through its reusable-block file, which
370 + // pages referencing it enqueue. Refresh (or remove) that file here so
371 + // edits to the pattern take effect without re-saving those pages.
372 + if ( 'wp_block' === $post_type ) {
373 + $betterdocs_blocks = array( );
374 + $recursive_response = is_array( $parsed_content ) ? CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks ) : array( );
375 + $this->write_reusable_block_css( CSSParser::blocks_to_style_array( $recursive_response ), $post_id );
376 + return;
377 + }
378 +
262 379 if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
263 380 $this->write_css_from_content( $post, $post_id, $parsed_content );
264 381 }
265 382 }
@@ -284,8 +401,90 @@
284 401
285 402 /**
286 403 * Get post content when page is load in frontend
287 404 */
405 + /**
406 + * Cheap pre-check: can this post's content hold BetterDocs blocks,
407 + * directly or through a reusable block (`core/block` ref)?
408 + *
409 + * @param \WP_Post|null $post Post.
410 + * @return bool
411 + */
412 + public static function may_have_betterdocs_blocks( $post ) {
413 + $content = isset( $post->post_content ) ? (string) $post->post_content : '';
414 + return false !== strpos( $content, '<!-- wp:betterdocs/' ) || false !== strpos( $content, '<!-- wp:block ' );
415 + }
416 +
417 + /**
418 + * Whether a synced pattern (wp_block) holds BetterDocs blocks.
419 + * get_post() is object-cached, so this is cheap per enqueue.
420 + *
421 + * @param int $id wp_block post ID.
422 + * @return bool
423 + */
424 + private function reusable_block_has_betterdocs_blocks( $id ) {
425 + $block = get_post( (int) $id );
426 + return isset( $block->post_content ) && false !== strpos( (string) $block->post_content, '<!-- wp:betterdocs/' );
427 + }
428 +
429 + /**
430 + * Refresh the widget pattern list at the end of the request: core fires
431 + * rest_after_save_widget before it assigns the widget to its sidebar.
432 + * @return void
433 + */
434 + public function schedule_widget_patterns_refresh() {
435 + if ( false === has_action( 'shutdown', array( $this, 'refresh_widget_patterns' ) ) ) {
436 + add_action( 'shutdown', array( $this, 'refresh_widget_patterns' ) );
437 + }
438 + }
439 +
440 + /**
441 + * Write the CSS of the synced patterns placed in active block widgets and
442 + * record their ids, which every page enqueues.
443 + * @return array Pattern ids.
444 + */
445 + public function refresh_widget_patterns() {
446 + $ids = array( );
447 + $instances = get_option( 'widget_block', array( ) );
448 + foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
449 + if ( 'wp_inactive_widgets' === $sidebar_id || ! is_array( $widget_ids ) ) {
450 + continue;
451 + }
452 + foreach ( $widget_ids as $widget_id ) {
453 + if ( ! preg_match( '/^block-(\d+)$/', (string) $widget_id, $matches ) || empty( $instances[ $matches[1] ]['content'] ) ) {
454 + continue;
455 + }
456 + $betterdocs_blocks = array( );
457 + $recursive_response = CSSParser::betterdocs_block_style_recursive( parse_blocks( $instances[ $matches[1] ]['content'] ), $betterdocs_blocks );
458 + $reusable_blocks = ! empty( $recursive_response['reusableBlocks'] ) ? $recursive_response['reusableBlocks'] : array( );
459 + foreach ( $reusable_blocks as $block_id => $block ) {
460 + $this->write_reusable_block_css( CSSParser::blocks_to_style_array( $block ), $block_id );
461 + if ( ! empty( $block ) ) {
462 + $ids[] = (int) $block_id;
463 + }
464 + }
465 + }
466 + }
467 +
468 + $ids = array_values( array_unique( $ids ) );
469 + update_option( '_betterdocs_widget_reusable_block_ids', $ids );
470 + return $ids;
471 + }
472 +
473 + /**
474 + * Enqueue a synced pattern's CSS file, if it holds BetterDocs blocks.
475 + * @return void
476 + */
477 + private function enqueue_reusable_block_style( $reusableId, $deps ) {
478 + // Only for synced patterns that hold BetterDocs blocks: files written
479 + // before #174 for patterns built with other plugins' blocks are stale
480 + // duplicates of their CSS (#174).
481 + $file = $this->style_dir . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css';
482 + if ( file_exists( $file ) && $this->reusable_block_has_betterdocs_blocks( $reusableId ) ) {
483 + wp_enqueue_style( 'betterdocs-reusable-block-style-' . $reusableId, $this->style_url . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css', $deps, $this->file_version( $file ) );
484 + }
485 + }
486 +
288 487 public function generate_post_content() {
289 488 $post_id = get_the_ID();
290 489 if ( $post_id ) {
291 490 $post_type = get_post_type( $post_id );
@@ -299,8 +498,16 @@
299 498 if ( false !== wp_is_post_revision( $post_id ) ) {
300 499 return null;
301 500 }
302 501
502 + // This runs on every uncached front-end request. Skip the parse
503 + // and the file/meta/option writes for content with nothing for
504 + // BetterDocs to style — on a large page built with other blocks
505 + // that was a full parse_blocks() plus a CSS rewrite per view.
506 + if ( ! self::may_have_betterdocs_blocks( $post ) ) {
507 + return;
508 + }
509 +
303 510 $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
304 511
305 512 if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
306 513 $this->write_css_from_content( $post, $post_id, $parsed_content );
@@ -308,8 +515,20 @@
308 515 }
309 516 }
310 517
311 518 /**
519 + * Cache-busting version for a generated stylesheet: its modification time.
520 + * Was a random value per render, which stopped browsers caching the file.
521 + *
522 + * @param string $path File path.
523 + * @return string|false
524 + */
525 + private function file_version( $path ) {
526 + $mtime = @filemtime( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
527 + return $mtime ? (string) $mtime : false;
528 + }
529 +
530 + /**
312 531 * Ajax callback to write css in upload directory
313 532 * @retun void
314 533 * @since 1.0.2
315 534 */
@@ -317,24 +536,33 @@
317 536 // Write CSS for FSE templates / template parts — one file per post so
318 537 // styles from different templates do not overwrite each other in a
319 538 // shared file (issue #53).
320 539 if ( isset( $post->post_type ) && ( 'wp_template_part' === $post->post_type || 'wp_template' === $post->post_type ) ) {
321 - if ( empty( $block_styles ) ) {
322 - return;
540 + $file = $this->style_dir . $this->prefix . '-edit-site-' . abs( $post->ID ) . '.min.css';
541 + } else { // Page/Posts
542 + $file = $this->style_dir . $this->prefix . '-' . abs( $post->ID ) . '.min.css';
543 + }
544 +
545 + $css = empty( $block_styles ) ? '' : CSSParser::build_css( $block_styles );
546 + if ( empty( $css ) ) {
547 + // No BetterDocs blocks left: drop the file so a stale one is not
548 + // enqueued forever (#174).
549 + if ( file_exists( $file ) ) {
550 + wp_delete_file( $file );
323 551 }
324 - if ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
325 - if ( ! file_exists( $this->style_dir ) ) {
326 - mkdir( $this->style_dir );
327 - }
328 - file_put_contents( $this->style_dir . $this->prefix . '-edit-site-' . abs( $post->ID ) . '.min.css', $css );
329 - }
330 - } // Write CSS for Page/Posts
331 - elseif ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
332 - if ( ! file_exists( $this->style_dir ) ) {
333 - mkdir( $this->style_dir );
334 - }
335 - file_put_contents( $this->style_dir . $this->prefix . '-' . abs( $post->ID ) . '.min.css', $css );
552 + return;
336 553 }
554 +
555 + // Unchanged CSS: leave the file (and its mtime-based version) alone,
556 + // so browsers keep their cached copy.
557 + if ( file_exists( $file ) && file_get_contents( $file ) === $css ) {
558 + return;
559 + }
560 +
561 + if ( ! file_exists( $this->style_dir ) ) {
562 + wp_mkdir_p( $this->style_dir );
563 + }
564 + file_put_contents( $file, $css );
337 565 }
338 566
339 567 /**
340 568 * Write css for Reusable block
@@ -341,63 +569,30 @@
341 569 * @retun void
342 570 * @since 3.4.0
343 571 */
344 572 private function write_reusable_block_css( $block_styles, $id ) {
345 - if ( isset( $block_styles ) && is_array( $block_styles ) ) {
346 - if ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
347 - $upload_dir = $this->style_dir . 'reusable-blocks/';
348 - if ( ! file_exists( $upload_dir ) ) {
349 - mkdir( $upload_dir, 0777, true );
350 - }
351 - file_put_contents( $upload_dir . '/' . 'betterdocs-reusable-' . abs( $id ) . '.min.css', $css );
573 + $upload_dir = $this->style_dir . 'reusable-blocks/';
574 + $file = $upload_dir . 'betterdocs-reusable-' . abs( $id ) . '.min.css';
575 +
576 + $css = empty( $block_styles ) || ! is_array( $block_styles ) ? '' : CSSParser::build_css( $block_styles );
577 + if ( empty( $css ) ) {
578 + // No BetterDocs blocks in this synced pattern: drop the file so a
579 + // stale one is not enqueued forever (#174).
580 + if ( file_exists( $file ) ) {
581 + wp_delete_file( $file );
352 582 }
583 + return;
353 584 }
354 - }
355 585
356 - /**
357 - * Single file css generator
358 - * @retun void
359 - * @since 3.5.3
360 - */
361 - private function single_file_css_generator( $block_styles, $upload_dir, $filename ) {
362 - $editSiteCssPath = $upload_dir . $filename;
363 - if ( file_exists( $editSiteCssPath ) ) {
364 - $existingCss = file_get_contents( $editSiteCssPath );
365 - $pattern = '~\/\*(.*?)\*\/~';
366 - preg_match_all( $pattern, $existingCss, $result, PREG_PATTERN_ORDER );
367 - $allComments = $result[ 0 ];
368 - $seperatedIds = array( );
369 - foreach ( $allComments as $comment ) {
370 - $id = preg_replace( '/[^A-Za-z0-9\-]|Ends|Starts/', '', $comment );
586 + // Unchanged CSS: leave the file (and its mtime-based version) alone.
587 + if ( file_exists( $file ) && file_get_contents( $file ) === $css ) {
588 + return;
589 + }
371 590
372 - if ( strpos( $comment, 'Starts' ) ) {
373 - $seperatedIds[ $id ][ 'start' ] = $comment;
374 - } elseif ( strpos( $comment, 'Ends' ) ) {
375 - $seperatedIds[ $id ][ 'end' ] = $comment;
376 - }
377 - }
378 -
379 - $seperateStyles = array( );
380 - foreach ( $seperatedIds as $key => $ids ) {
381 - $seperateStyles[ ][ $key ] = isset( $block_styles[ $key ] ) ? $block_styles[ $key ] : array( );
382 - }
383 -
384 - self::$_block_styles = array_merge( self::$_block_styles, $block_styles );
385 -
386 - if ( ! empty( $css = CSSParser::build_css( self::$_block_styles ) ) ) {
387 - if ( ! file_exists( $upload_dir ) ) {
388 - mkdir( $upload_dir );
389 - }
390 -
391 - file_put_contents( $editSiteCssPath, $css );
392 - }
393 - } elseif ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
394 - if ( ! file_exists( $this->style_dir ) ) {
395 - mkdir( $this->style_dir );
396 - }
397 -
398 - file_put_contents( $editSiteCssPath, $css );
591 + if ( ! file_exists( $upload_dir ) ) {
592 + wp_mkdir_p( $upload_dir );
399 593 }
594 + file_put_contents( $file, $css );
400 595 }
401 596
402 597 /**
403 598 * Get post id by post_name for template
@@ -405,7 +600,8 @@
405 600 public static function betterdocs_get_post_content_by_post_name( $post_name ) {
406 601 global $wpdb;
407 602 $sql = $wpdb->prepare( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_name = %s", $post_name );
408 603
409 - return $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
604 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off lookup of FSE template posts by post_name during CSS generation; no caching layer applies.
605 + return $wpdb->get_results( $sql, ARRAY_A );
410 606 }
411 607 }