diff --git a/bun.lock b/bun.lock index 232f475..7d63639 100644 --- a/bun.lock +++ b/bun.lock @@ -75,7 +75,7 @@ }, "packages/figma": { "name": "core-framework-figma", - "version": "2.0.0", + "version": "2.0.2", "dependencies": { "@fontsource-variable/inter": "^5.3.0", "colord": "^2.9.3", @@ -102,7 +102,7 @@ }, "packages/gutenberg": { "name": "coreframework-gutenberg-integration", - "version": "2.0.0", + "version": "2.0.2", "devDependencies": { "@wordpress/block-editor": "^16.1.0", "@wordpress/browserslist-config": "^6.53.0", @@ -119,7 +119,7 @@ }, "packages/wp": { "name": "coreframework", - "version": "2.0.0", + "version": "2.0.2", "dependencies": { "@adobe/react-spectrum": "^3.47.3", "@core-framework/core": "workspace:*", @@ -167,7 +167,7 @@ }, "packages/www": { "name": "core-framework", - "version": "2.0.0", + "version": "2.0.2", "dependencies": { "@adobe/react-spectrum": "^3.47.3", "@core-framework/core": "workspace:*", @@ -229,6 +229,8 @@ "micromatch": "4.0.8", "nanoid": "3.3.18", "postcss": "8.5.26", + "proxy-addr": "2.0.8", + "shell-quote": "1.11.0", "svgo": "3.3.4", "websocket-driver": "0.7.5", "yaml": "1.10.3", @@ -3178,7 +3180,7 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], @@ -3386,7 +3388,7 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shell-quote": ["shell-quote@1.9.0", "", {}, "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA=="], + "shell-quote": ["shell-quote@1.11.0", "", {}, "sha512-JdxDPD0DBTyu08pq0kPC0xSNet/qsU07qT6IsX1AS8oO2ICNRY4ldNa8OAI6PuwAH8tG3lxEhbqmyp4Dw4036g=="], "shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="], diff --git a/package.json b/package.json index a9902c5..a6993d8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "micromatch": "4.0.8", "nanoid": "3.3.18", "postcss": "8.5.26", + "proxy-addr": "2.0.8", + "shell-quote": "1.11.0", "svgo": "3.3.4", "websocket-driver": "0.7.5", "yaml": "1.10.3" diff --git a/packages/wp/Tests/BricksSynchronizationTest.php b/packages/wp/Tests/BricksSynchronizationTest.php index 8c31885..ce44aee 100644 --- a/packages/wp/Tests/BricksSynchronizationTest.php +++ b/packages/wp/Tests/BricksSynchronizationTest.php @@ -68,8 +68,11 @@ function get_post_types( $args = array(), $output = 'names' ) { if ( ! function_exists( 'get_posts' ) ) { function get_posts( $args = array() ) { - $meta_keys = array_column( array_filter( $args['meta_query'] ?? array(), 'is_array' ), 'key' ); - $post_ids = array(); + $meta_keys = array(); + foreach ( array_filter( $args['meta_query'] ?? array(), 'is_array' ) as $clause ) { + $meta_keys = array_merge( $meta_keys, (array) $clause['key'] ); + } + $post_ids = array(); foreach ( $GLOBALS['cf_test_post_meta'] as $post_id => $post_meta ) { if ( array_intersect( $meta_keys, array_keys( $post_meta ) ) ) { @@ -447,6 +450,42 @@ public function testSelectorsThatSanitizeToTheSameIdProduceOneClass(): void { ); } + public function testReferenceSweepFindsEveryBricksMetaKeyAndSkipsUnrelatedPosts(): void { + $meta_keys = array( + '_bricks_page_header_2', + '_bricks_page_content_2', + '_bricks_page_footer_2', + '_bricks_page_settings', + '_bricks_template_settings', + ); + + foreach ( $meta_keys as $index => $meta_key ) { + $GLOBALS['cf_test_post_meta'][ $index + 1 ] = array( + $meta_key => array( '_cssGlobalClasses' => array( 'removed_c', 'keep-me' ) ), + ); + } + $GLOBALS['cf_test_post_meta'][6] = array( '_bricks_page_content_2' => '' ); + $GLOBALS['cf_test_post_meta'][7] = array( '_bricks_page_settings' => array() ); + $GLOBALS['cf_test_post_meta'][8] = array( + 'unrelated_meta' => array( '_cssGlobalClasses' => array( 'removed_c' ) ), + ); + + $method = new ReflectionMethod( BricksFunctions::class, 'remove_class_references' ); + $method->setAccessible( true ); + $method->invoke( $this->createBricksFunctions(), array( 'removed_c' ) ); + + $this->assertSame( array( range( 1, 7 ) ), $GLOBALS['cf_test_meta_primes'] ); + foreach ( $meta_keys as $index => $meta_key ) { + $this->assertSame( + array( 'keep-me' ), + $GLOBALS['cf_test_post_meta'][ $index + 1 ][ $meta_key ]['_cssGlobalClasses'] + ); + } + $this->assertSame( '', $GLOBALS['cf_test_post_meta'][6]['_bricks_page_content_2'] ); + $this->assertSame( array(), $GLOBALS['cf_test_post_meta'][7]['_bricks_page_settings'] ); + $this->assertSame( array( 'removed_c' ), $GLOBALS['cf_test_post_meta'][8]['unrelated_meta']['_cssGlobalClasses'] ); + } + public function testReferenceSweepIsBatchedAndRunsAfterClassesArePersisted(): void { $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_CLASSES_OPTION ] = array( array( diff --git a/packages/wp/wp/App/Bricks/Functions.php b/packages/wp/wp/App/Bricks/Functions.php index f872a6f..1edcdc9 100644 --- a/packages/wp/wp/App/Bricks/Functions.php +++ b/packages/wp/wp/App/Bricks/Functions.php @@ -254,13 +254,15 @@ private function remove_class_references( array $class_ids ): void { defined( 'BRICKS_DB_TEMPLATE_SETTINGS' ) ? constant( 'BRICKS_DB_TEMPLATE_SETTINGS' ) : '_bricks_template_settings', ); - $meta_query = array( 'relation' => 'OR' ); - foreach ( $meta_keys as $meta_key ) { - $meta_query[] = array( - 'key' => $meta_key, - 'compare' => 'EXISTS', - ); - } + // Separate OR/EXISTS clauses create one postmeta join per key, multiplying + // rows on posts with lots of metadata. Match all keys through a single join. + $meta_query = array( + array( + 'key' => $meta_keys, + 'compare_key' => 'IN', + 'compare' => 'EXISTS', + ), + ); $post_ids = get_posts( array( diff --git a/scripts/test-wp-e2e.sh b/scripts/test-wp-e2e.sh index 11030f8..4fd4819 100755 --- a/scripts/test-wp-e2e.sh +++ b/scripts/test-wp-e2e.sh @@ -141,6 +141,33 @@ if (!is_file($path)) { WP_CLI::success("Generated stylesheet exists in the uploads directory."); ' +# Exercise the real WP_Query SQL builder: unit tests use a get_posts() stub. +# Short-circuit execution so this regression check never runs a Cartesian join. +wp_cli eval ' +$queries = array(); +$capture = static function ($posts, $query) use (&$queries) { + $queries[] = $query->request; + return array(); +}; +add_filter("posts_pre_query", $capture, 10, 2); +try { + $class = new ReflectionClass(\CoreFramework\App\Bricks\Functions::class); + $method = $class->getMethod("remove_class_references"); + $method->setAccessible(true); + $method->invoke($class->newInstanceWithoutConstructor(), array("cf-e2e-removed_c")); +} finally { + remove_filter("posts_pre_query", $capture, 10); +} +global $wpdb; +if (count($queries) !== 1 || substr_count($queries[0], "JOIN $wpdb->postmeta") !== 1) { + WP_CLI::error("Bricks reference discovery must use exactly one postmeta join."); +} +if (strpos($queries[0], "$wpdb->postmeta.meta_key IN (") === false) { + WP_CLI::error("Bricks reference discovery must filter metadata keys together."); +} +WP_CLI::success("Bricks reference discovery uses one postmeta join."); +' + curl --fail --silent --show-error --cookie-jar "$COOKIE_FILE" "$SITE_URL/wp-login.php" >/dev/null curl --fail --silent --show-error --location \ --cookie "$COOKIE_FILE" \