From a979b40903f92715380f6dd1bf3ab81a8e71ae0e Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Tue, 18 Aug 2026 11:38:18 +0300 Subject: [PATCH 01/25] fix: guard widgets CSS writability check against missing WP_Filesystem() (#2938) CSS_Handler::is_writable() called WP_Filesystem() before its function_exists() fallback, so a frontend request with an active sidebar and no generated widget stylesheet fataled with "Call to undefined function WP_Filesystem()" when the include did not expose the function. Run the guard first so the request degrades to the inline widget CSS fallback, and verify the initialized $wp_filesystem instance before reading its method. Adds an isolated-process PHPUnit regression test and a frontend e2e spec covering the blocked-filesystem fallback and the written-file path. Fixes #2937 Co-authored-by: Luca Dobrescu Co-authored-by: Claude Fable 5 --- .wp-env.json | 5 +- inc/css/class-css-handler.php | 9 +- .../mu-plugins/otter-e2e-bootstrap.php | 167 ++++++++++++++++++ .../e2e/blocks/widgets-css-frontend.spec.js | 94 ++++++++++ src/blocks/test/e2e/fixtures.ts | 15 ++ src/blocks/test/e2e/playwright.config.js | 5 +- tests/php/is-writable-sandbox.php | 65 +++++++ tests/test-css-handler.php | 50 ++++++ 8 files changed, 403 insertions(+), 7 deletions(-) create mode 100644 src/blocks/test/e2e/blocks/widgets-css-frontend.spec.js create mode 100644 tests/php/is-writable-sandbox.php create mode 100644 tests/test-css-handler.php diff --git a/.wp-env.json b/.wp-env.json index ecc59bbe2..baf5a856f 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -6,7 +6,10 @@ ".", "https://downloads.wordpress.org/plugin/ai-provider-for-openai.zip" ], - "themes": [ "./test/emptytheme" ], + "themes": [ + "./test/emptytheme", + "https://downloads.wordpress.org/theme/twentytwentyone.zip" + ], "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, diff --git a/inc/css/class-css-handler.php b/inc/css/class-css-handler.php index db96b5832..41afb4ecd 100644 --- a/inc/css/class-css-handler.php +++ b/inc/css/class-css-handler.php @@ -496,18 +496,17 @@ public static function save_widgets_styles() { public static function is_writable() { global $wp_filesystem; include_once ABSPATH . 'wp-admin/includes/file.php'; - WP_Filesystem(); - - $wp_upload_dir = wp_upload_dir( null, false ); - $upload_dir = $wp_upload_dir['basedir']; if ( ! function_exists( 'WP_Filesystem' ) ) { return false; } + $wp_upload_dir = wp_upload_dir( null, false ); + $upload_dir = $wp_upload_dir['basedir']; + $writable = WP_Filesystem( false, $upload_dir ); - return $writable && 'direct' === $wp_filesystem->method; + return $writable && $wp_filesystem instanceof \WP_Filesystem_Base && 'direct' === $wp_filesystem->method; } /** diff --git a/packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php b/packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php index 882241e0a..6288186ff 100644 --- a/packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php +++ b/packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php @@ -94,6 +94,19 @@ */ const OPENAI_STUB_OPTION = 'otter_e2e_openai_stub'; +/** + * When truthy, get_filesystem_method() reports a bogus method so WP_Filesystem() + * fails to initialize. Simulates hosts where the filesystem API is unavailable + * on the frontend (issue #2937) — CSS_Handler::is_writable() must degrade to + * `false` and the widgets CSS must fall back to an inline '; + echo ''; } /** diff --git a/src/blocks/test/e2e/blocks/atomic-wind-hide-conditions.spec.js b/src/blocks/test/e2e/blocks/atomic-wind-hide-conditions.spec.js new file mode 100644 index 000000000..d28873609 --- /dev/null +++ b/src/blocks/test/e2e/blocks/atomic-wind-hide-conditions.spec.js @@ -0,0 +1,90 @@ +/** + * Internal dependencies + */ +import { test, expect } from '../fixtures'; +import { setAtomicWind } from '../helpers/design-library'; +import { publishAndViewPost } from '../helpers/editor'; + +/** + * Atomic Wind compiles Tailwind in important mode, so a `flex` utility lands as + * `display:flex!important`. The "Hide on" screen-size condition is also + * `!important`, so unless its rules outrank the utilities the block stays + * visible on the device it was hidden for, depending on which stylesheet the + * page happened to print last. + */ +const MOBILE = { width: 375, height: 700 }; +const TABLET = { width: 900, height: 700 }; +const DESKTOP = { width: 1280, height: 700 }; + +const insertHiddenBox = ( editor, screenSizes ) => + editor.insertBlock({ + name: 'atomic-wind/box', + attributes: { + className: 'flex gap-4 p-8', + otterConditions: [ + [ + { + type: 'screenSize', + screen_sizes: screenSizes + } + ] + ] + }, + innerBlocks: [ + { name: 'core/paragraph', attributes: { content: 'Boxed content' }} + ] + }); + +test.describe( 'Atomic Wind hide on screen size', () => { + test.beforeEach( async({ otterUtils, admin }) => { + await setAtomicWind( otterUtils, true ); + await admin.createNewPost(); + }); + + test.afterAll( async({ otterUtils }) => { + await setAtomicWind( otterUtils, false ); + }); + + test( 'hiding on mobile beats the flex utility', async({ editor, page }) => { + await insertHiddenBox( editor, [ 'mobile' ] ); + await publishAndViewPost({ editor, page }); + + const box = page.locator( '.wp-block-atomic-wind-box' ); + + await expect( box ).toHaveClass( /o-hide-on-mobile/ ); + + // Outside the hidden range the utility must still apply, so a passing + // hidden assertion cannot come from a stylesheet that never loaded. + await page.setViewportSize( TABLET ); + await expect( box ).toHaveCSS( 'display', 'flex' ); + + await page.setViewportSize( MOBILE ); + await expect( box ).toHaveCSS( 'display', 'none' ); + }); + + test( 'hiding on tablet beats the flex utility', async({ editor, page }) => { + await insertHiddenBox( editor, [ 'tablet' ] ); + await publishAndViewPost({ editor, page }); + const box = page.locator( '.wp-block-atomic-wind-box' ); + await expect( box ).toHaveClass( /o-hide-on-tablet/ ); + await page.setViewportSize( MOBILE ); + await expect( box ).toHaveCSS( 'display', 'flex' ); + await page.setViewportSize( TABLET ); + await expect( box ).toHaveCSS( 'display', 'none' ); + }); + + test( 'hiding on desktop beats the flex utility', async({ editor, page }) => { + await insertHiddenBox( editor, [ 'desktop' ] ); + await publishAndViewPost({ editor, page }); + + const box = page.locator( '.wp-block-atomic-wind-box' ); + + await expect( box ).toHaveClass( /o-hide-on-desktop/ ); + + await page.setViewportSize( MOBILE ); + await expect( box ).toHaveCSS( 'display', 'flex' ); + + await page.setViewportSize( DESKTOP ); + await expect( box ).toHaveCSS( 'display', 'none' ); + }); +}); diff --git a/src/blocks/test/e2e/playwright.config.js b/src/blocks/test/e2e/playwright.config.js index e9458d047..1e8d3105b 100644 --- a/src/blocks/test/e2e/playwright.config.js +++ b/src/blocks/test/e2e/playwright.config.js @@ -59,6 +59,7 @@ const SERIAL_SPECS = [ // Flips the site-wide atomic-wind blocks option. '**/blocks/atomic-wind-list-view.spec.js', '**/blocks/atomic-wind-css-scope.spec.js', + '**/blocks/atomic-wind-hide-conditions.spec.js', // Mutates the shared admin user's metabox order and editor preferences. '**/blocks/woocommerce-builder.spec.js', From 30fe4398a0f01b9139fa0bdee3e00e5e55b5bb23 Mon Sep 17 00:00:00 2001 From: Marius Cristea Date: Wed, 2 Sep 2026 13:18:29 +0300 Subject: [PATCH 23/25] ci: skip PR-comment job on Dependabot PRs (no secrets, always fails) Co-Authored-By: Claude Fable 5 --- .github/workflows/create-build-url.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create-build-url.yml b/.github/workflows/create-build-url.yml index 69ef9497f..52afbc225 100644 --- a/.github/workflows/create-build-url.yml +++ b/.github/workflows/create-build-url.yml @@ -55,7 +55,7 @@ jobs: comment-on-pr: name: Comment on PR with links to plugin ZIPs - if: ${{ github.head_ref && github.head_ref != null }} + if: ${{ github.head_ref && github.head_ref != null && github.actor != 'dependabot[bot]' }} runs-on: ubuntu-latest needs: dev-zip env: From 74da395509303820e872b3d1f6994feae253a539 Mon Sep 17 00:00:00 2001 From: Marius Cristea Date: Wed, 2 Sep 2026 13:24:35 +0300 Subject: [PATCH 24/25] ci: revert Dependabot gate on PR-comment job (org Dependabot secret covers this repo) [skip ci] Co-Authored-By: Claude Fable 5 --- .github/workflows/create-build-url.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create-build-url.yml b/.github/workflows/create-build-url.yml index 52afbc225..69ef9497f 100644 --- a/.github/workflows/create-build-url.yml +++ b/.github/workflows/create-build-url.yml @@ -55,7 +55,7 @@ jobs: comment-on-pr: name: Comment on PR with links to plugin ZIPs - if: ${{ github.head_ref && github.head_ref != null && github.actor != 'dependabot[bot]' }} + if: ${{ github.head_ref && github.head_ref != null }} runs-on: ubuntu-latest needs: dev-zip env: From 73b3c5a4447cdcea4e638e320d1864f933654c71 Mon Sep 17 00:00:00 2001 From: Girish Panchal <79647963+girishpanchal30@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:31:12 +0530 Subject: [PATCH 25/25] Improved SVG metadata generation (#3022) * fix: improve SVG metadata generation * fix: function documentation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- inc/class-main.php | 20 ++++++- tests/test-svg-upload.php | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/inc/class-main.php b/inc/class-main.php index 39d1aed7d..ca415d8a3 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -529,11 +529,29 @@ public function generate_svg_attachment_metadata( $metadata, $attachment_id ) { } $svg_path = get_attached_file( $attachment_id ); + + if ( empty( $svg_path ) || ! file_exists( $svg_path ) || ! is_readable( $svg_path ) ) { + return $metadata; + } + $filename = basename( $svg_path ); - $svg = simplexml_load_file( $svg_path ); + // Keep malformed SVG errors internal instead of emitting PHP warnings. + $previous_state = libxml_use_internal_errors( true ); + $svg = simplexml_load_file( $svg_path ); + libxml_clear_errors(); + libxml_use_internal_errors( $previous_state ); + + if ( false === $svg ) { + return $metadata; + } + $attributes = $svg->attributes(); + if ( ! isset( $attributes->width, $attributes->height ) ) { + return $metadata; + } + // Update metadata with SVG dimensions. $metadata['width'] = intval( (string) $attributes->width ); $metadata['height'] = intval( (string) $attributes->height ); diff --git a/tests/test-svg-upload.php b/tests/test-svg-upload.php index 4f176a747..314df2c92 100644 --- a/tests/test-svg-upload.php +++ b/tests/test-svg-upload.php @@ -209,4 +209,120 @@ public function test_used_css_properties_returns_default_when_input_is_not_array $this->assertContains( 'border-radius', $result ); $this->assertContains( 'transform', $result ); } + + /** + * Create an SVG attachment post for the supplied file path. + * + * @param string $file Attached file path to store, may be empty. + * @return int + */ + private function create_svg_attachment( $file ) { + $attachment_id = $this->factory()->attachment->create_object( + array( + 'file' => $file, + 'post_mime_type' => 'image/svg+xml', + ) + ); + + return $attachment_id; + } + + /** + * A missing SVG file must not fatal, and metadata is returned untouched. + */ + public function test_generate_svg_attachment_metadata_with_missing_file() { + $main = new ThemeIsle\GutenbergBlocks\Main(); + $attachment_id = $this->create_svg_attachment( '/does/not/exist/missing.svg' ); + $metadata = array( 'sizes' => array() ); + + $result = $main->generate_svg_attachment_metadata( $metadata, $attachment_id ); + + $this->assertSame( $metadata, $result ); + + wp_delete_attachment( $attachment_id, true ); + } + + /** + * get_attached_file() returning false must not fatal. + */ + public function test_generate_svg_attachment_metadata_when_attached_file_is_false() { + $main = new ThemeIsle\GutenbergBlocks\Main(); + $attachment_id = $this->create_svg_attachment( '' ); + $metadata = array( 'sizes' => array() ); + + $force_false = '__return_false'; + add_filter( 'get_attached_file', $force_false ); + + $result = $main->generate_svg_attachment_metadata( $metadata, $attachment_id ); + + remove_filter( 'get_attached_file', $force_false ); + + $this->assertSame( $metadata, $result ); + + wp_delete_attachment( $attachment_id, true ); + } + + /** + * A file that is not valid XML must not fatal. + */ + public function test_generate_svg_attachment_metadata_with_malformed_svg() { + $main = new ThemeIsle\GutenbergBlocks\Main(); + $svg_path = wp_tempnam( 'broken.svg' ); + file_put_contents( $svg_path, '' ); + + $attachment_id = $this->create_svg_attachment( $svg_path ); + $metadata = array( 'sizes' => array() ); + + $result = $main->generate_svg_attachment_metadata( $metadata, $attachment_id ); + + $this->assertSame( $metadata, $result ); + + wp_delete_attachment( $attachment_id, true ); + if ( file_exists( $svg_path ) ) { + unlink( $svg_path ); + } + } + + /** + * A valid SVG still gets its dimensions filled in. + */ + public function test_generate_svg_attachment_metadata_with_valid_svg() { + $main = new ThemeIsle\GutenbergBlocks\Main(); + $svg_path = wp_tempnam( 'valid.svg' ); + file_put_contents( $svg_path, '' ); + + $attachment_id = $this->create_svg_attachment( $svg_path ); + + $result = $main->generate_svg_attachment_metadata( array( 'sizes' => array() ), $attachment_id ); + + $this->assertSame( 24, $result['width'] ); + $this->assertSame( 42, $result['height'] ); + $this->assertSame( basename( $svg_path ), $result['file'] ); + + wp_delete_attachment( $attachment_id, true ); + if ( file_exists( $svg_path ) ) { + unlink( $svg_path ); + } + } + + /** + * An SVG without width/height attributes must not produce zeroed dimensions. + */ + public function test_generate_svg_attachment_metadata_without_dimension_attributes() { + $main = new ThemeIsle\GutenbergBlocks\Main(); + $svg_path = wp_tempnam( 'no-dimensions.svg' ); + file_put_contents( $svg_path, '' ); + + $attachment_id = $this->create_svg_attachment( $svg_path ); + $metadata = array( 'sizes' => array() ); + + $result = $main->generate_svg_attachment_metadata( $metadata, $attachment_id ); + + $this->assertSame( $metadata, $result ); + + wp_delete_attachment( $attachment_id, true ); + if ( file_exists( $svg_path ) ) { + unlink( $svg_path ); + } + } }