| @@ -276,9 +276,22 @@ | ||
| 276 | 276 | * |
| 277 | 277 | * @param array{media:string,async:bool,tags:string[],urls:string[]} $run Run to merge. |
| 278 | 278 | */ |
| 279 | 279 | private static function merge_run( array $run ): ?string { |
| 280 | - $key = md5( implode( '|', array_map( static fn( $p ) => $p . ':' . (int) @filemtime( $p ), $run['urls'] ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing file contributes 0 to the key; handled below. | |
| 280 | + // Nowhere to write means nothing to link: a <link> to a file that was | |
| 281 | + // never written unstyles the page. | |
| 282 | + if ( ! Minifier::min_dir_writable() ) { | |
| 283 | + return null; | |
| 284 | + } | |
| 285 | + // Named by content and by site. See Asset_Combiner::combined_key(). | |
| 286 | + $parts = array(); | |
| 287 | + foreach ( $run['urls'] as $path ) { | |
| 288 | + $parts[] = array( | |
| 289 | + 'path' => $path, | |
| 290 | + 'url' => self::path_to_url( $path ), | |
| 291 | + ); | |
| 292 | + } | |
| 293 | + $key = Asset_Combiner::combined_key( $parts, 'css' ); | |
| 281 | 294 | $dir = Asset_Combiner::cache_dir(); |
| 282 | 295 | $file = $dir . '/combined-' . $key . '.css'; |
| 283 | 296 | $url = Asset_Combiner::cache_url() . '/combined-' . $key . '.css'; |
| 284 | 297 | |
| @@ -324,10 +337,13 @@ | ||
| 324 | 337 | $css = Asset_Combiner::minify_css_body( $css ); |
| 325 | 338 | if ( ! is_dir( $dir ) ) { |
| 326 | 339 | wp_mkdir_p( $dir ); |
| 327 | 340 | } |
| 328 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents -- WP_Filesystem requires admin context, unavailable on the frontend. | |
| 329 | - file_put_contents( $file, $css, LOCK_EX ); | |
| 341 | + // Atomic: a concurrent render sees the whole file or none of it. | |
| 342 | + // A failed write links nothing rather than a file that is not there. | |
| 343 | + if ( ! Asset_Manifest::write_atomic( $file, $css ) && ! file_exists( $file ) ) { | |
| 344 | + return null; | |
| 345 | + } | |
| 330 | 346 | } |
| 331 | 347 | |
| 332 | 348 | // Carry Async CSS across the merge (issue #330). Every sheet in the |
| 333 | 349 | // run was async'd — that is a condition of grouping them, enforced in |
| @@ -454,10 +470,27 @@ | ||
| 454 | 470 | // and says the same thing: this sheet is parked under `print` and |
| 455 | 471 | // becomes something else on load. Any plugin using the standard |
| 456 | 472 | // print/swap idiom is read correctly rather than merged into a |
| 457 | 473 | // print-only bundle and stripped of its handler (#335 review, issue 3). |
| 458 | - if ( preg_match( '#\bonload\s*=\s*(["\'])\s*this\.media\s*=\s*(["\'])([^"\']*)\2#i', $tag, $m ) ) { | |
| 459 | - return $m[3]; | |
| 474 | + // | |
| 475 | + // The assignment can sit anywhere in the handler. loadCSS spells it | |
| 476 | + // `this.onload=null;this.media='all'`, and matching only a handler | |
| 477 | + // that STARTS with `this.media` filed two such sheets as real print | |
| 478 | + // sheets: merged into a `media="print"` bundle with no onload, they | |
| 479 | + // never applied on screen. (#560) | |
| 480 | + // | |
| 481 | + // The attribute is decoded first: a tag built with esc_attr() carries | |
| 482 | + // `this.media='all'`. An assignment whose value is not a | |
| 483 | + // literal (`this.media=this.dataset.media`) is still a swap; it | |
| 484 | + // restores to the screen, so it is read as `all`. | |
| 485 | + if ( preg_match( '#(?<![-\w])onload\s*=\s*(?:"([^"]*)"|\'([^\']*)\')#i', $tag, $h ) ) { | |
| 486 | + $handler = html_entity_decode( '' !== $h[1] ? $h[1] : ( $h[2] ?? '' ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); | |
| 487 | + if ( preg_match( '#\bthis\.media\s*=\s*(["\'])([^"\']*)\1#i', $handler, $m ) ) { | |
| 488 | + return $m[2]; | |
| 489 | + } | |
| 490 | + if ( preg_match( '#\bthis\.media\s*=(?!=)#i', $handler ) ) { | |
| 491 | + return 'all'; | |
| 492 | + } | |
| 460 | 493 | } |
| 461 | 494 | |
| 462 | 495 | return ''; |
| 463 | 496 | } |