From 9b915ecb25f513bbfe178282919b70bcab43d820 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Mon, 24 Aug 2026 15:08:52 +0530 Subject: [PATCH 1/4] fix: improve post element ordering --- inc/admin/metabox/manager.php | 9 +- inc/customizer/controls/ordering.php | 2 +- inc/customizer/options/layout_blog.php | 10 +- inc/customizer/options/layout_single_post.php | 17 +- inc/views/pluggable/metabox_settings.php | 6 +- inc/views/post_layout.php | 9 +- tests/stubs/woocommerce-cart.php | 27 ++ tests/test-neve-content-ordering.php | 177 --------- tests/test-neve-ordering.php | 372 ++++++++++++++++++ 9 files changed, 440 insertions(+), 189 deletions(-) delete mode 100644 tests/test-neve-content-ordering.php create mode 100644 tests/test-neve-ordering.php diff --git a/inc/admin/metabox/manager.php b/inc/admin/metabox/manager.php index 9c336e4dbe..d44db72ac8 100755 --- a/inc/admin/metabox/manager.php +++ b/inc/admin/metabox/manager.php @@ -454,10 +454,13 @@ private function get_post_elements_default_order() { $default_order = $this->get_v4_defaults( 'neve_layout_single_post_elements_order', $this->post_ordering() ); $content_order = get_theme_mod( 'neve_layout_single_post_elements_order', wp_json_encode( $default_order ) ); - if ( ! is_string( $content_order ) ) { - $content_order = wp_json_encode( $default_order ); + if ( is_string( $content_order ) ) { + $content_order = json_decode( $content_order, true ); + } + + if ( ! is_array( $content_order ) ) { + $content_order = $default_order; } - $content_order = json_decode( $content_order, true ); if ( empty( $content_order ) ) { return wp_json_encode( $content_order ); } diff --git a/inc/customizer/controls/ordering.php b/inc/customizer/controls/ordering.php index 1a0ba42a5b..334558538f 100644 --- a/inc/customizer/controls/ordering.php +++ b/inc/customizer/controls/ordering.php @@ -127,7 +127,7 @@ private function get_component_status_class( $component ) { if ( empty( $value ) ) { return ' enabled'; } - $value = json_decode( $value, true ); + $value = is_string( $value ) ? json_decode( $value, true ) : $value; if ( ! is_array( $value ) ) { $value = array(); diff --git a/inc/customizer/options/layout_blog.php b/inc/customizer/options/layout_blog.php index 98e609aa51..9eac1eec05 100644 --- a/inc/customizer/options/layout_blog.php +++ b/inc/customizer/options/layout_blog.php @@ -816,7 +816,15 @@ public function should_show_masonry() { return false; } - $columns = json_decode( get_theme_mod( 'neve_grid_layout', $this->grid_columns_default() ), true ); + $columns = get_theme_mod( 'neve_grid_layout', $this->grid_columns_default() ); + if ( is_string( $columns ) ) { + $columns = json_decode( $columns, true ); + } + + if ( ! is_array( $columns ) ) { + return false; + } + $columns = array_filter( array_values( $columns ), function ( $value ) { diff --git a/inc/customizer/options/layout_single_post.php b/inc/customizer/options/layout_single_post.php index 219a57c9fa..425b728efa 100644 --- a/inc/customizer/options/layout_single_post.php +++ b/inc/customizer/options/layout_single_post.php @@ -878,7 +878,14 @@ public function element_is_enabled( $element ) { ); $content_order = get_theme_mod( 'neve_layout_single_post_elements_order', wp_json_encode( $default_order ) ); - $content_order = json_decode( $content_order, true ); + if ( is_string( $content_order ) ) { + $content_order = json_decode( $content_order, true ); + } + + if ( ! is_array( $content_order ) ) { + $content_order = $default_order; + } + if ( ! in_array( $element, $content_order, true ) ) { return false; } @@ -906,7 +913,11 @@ public function sanitize_post_elements_ordering( $value ) { return wp_json_encode( $allowed ); } - $decoded = json_decode( $value, true ); + $decoded = is_string( $value ) ? json_decode( $value, true ) : $value; + + if ( ! is_array( $decoded ) || empty( $decoded ) ) { + return wp_json_encode( $allowed ); + } foreach ( $decoded as $val ) { if ( ! in_array( $val, $allowed, true ) ) { @@ -914,7 +925,7 @@ public function sanitize_post_elements_ordering( $value ) { } } - return $value; + return wp_json_encode( array_values( $decoded ) ); } /** diff --git a/inc/views/pluggable/metabox_settings.php b/inc/views/pluggable/metabox_settings.php index e970079bdf..3f449cebc4 100644 --- a/inc/views/pluggable/metabox_settings.php +++ b/inc/views/pluggable/metabox_settings.php @@ -556,7 +556,11 @@ public function filter_post_elements( $elements_order ) { return $elements_order; } - return json_decode( $meta_elements_order, true ); + if ( is_string( $meta_elements_order ) ) { + $meta_elements_order = json_decode( $meta_elements_order, true ); + } + + return is_array( $meta_elements_order ) ? $meta_elements_order : $elements_order; } /** diff --git a/inc/views/post_layout.php b/inc/views/post_layout.php index 856ae0770d..07bcf3467f 100644 --- a/inc/views/post_layout.php +++ b/inc/views/post_layout.php @@ -214,10 +214,13 @@ private function get_content_order() { $default_order = $this->get_v4_defaults( 'neve_layout_single_post_elements_order', $this->post_ordering() ); $content_order = get_theme_mod( 'neve_layout_single_post_elements_order', wp_json_encode( $default_order ) ); - if ( ! is_string( $content_order ) ) { - $content_order = wp_json_encode( $default_order ); + if ( is_string( $content_order ) ) { + $content_order = json_decode( $content_order, true ); + } + + if ( ! is_array( $content_order ) ) { + $content_order = $default_order; } - $content_order = json_decode( $content_order, true ); if ( apply_filters( 'neve_filter_toggle_content_parts', true, 'title' ) !== true ) { $title_key = array_search( 'title-meta', $content_order, true ); if ( $title_key !== false ) { diff --git a/tests/stubs/woocommerce-cart.php b/tests/stubs/woocommerce-cart.php index 7a9e08a942..d74b2f1f43 100644 --- a/tests/stubs/woocommerce-cart.php +++ b/tests/stubs/woocommerce-cart.php @@ -158,3 +158,30 @@ function is_checkout() { return false; } } + +if ( ! function_exists( 'is_shop' ) ) { + /** + * Stand-in for the shop page conditional. + * + * The `WooCommerce` class above stays declared for the rest of the run, so any code + * gated on `class_exists( 'WooCommerce' )` reaches this too. + * + * @return bool + */ + function is_shop() { + return false; + } +} + +if ( ! function_exists( 'wc_get_page_id' ) ) { + /** + * Stand-in for the WooCommerce page id lookup. + * + * @param string $page the page slug. + * + * @return int + */ + function wc_get_page_id( $page ) { + return -1; + } +} diff --git a/tests/test-neve-content-ordering.php b/tests/test-neve-content-ordering.php deleted file mode 100644 index bbfffb5abb..0000000000 --- a/tests/test-neve-content-ordering.php +++ /dev/null @@ -1,177 +0,0 @@ -get_ordered_components( $associative ); - } - - /** - * The default is used when no mod is set. - */ - public function test_returns_defaults_when_mod_missing() { - $this->assertSame( $this->defaults, $this->get_ordered_components() ); - $this->assertSame( $this->defaults, $this->get_ordered_components( true ) ); - } - - /** - * A JSON string mod - the shape the Customizer control writes - is decoded. - */ - public function test_decodes_json_string_mod() { - set_theme_mod( 'neve_post_content_ordering', wp_json_encode( array( 'title-meta', 'thumbnail' ) ) ); - - $this->assertSame( array( 'title-meta', 'thumbnail' ), $this->get_ordered_components( true ) ); - } - - /** - * An array-valued mod is returned as-is instead of fataling in json_decode(). - */ - public function test_array_mod_does_not_fatal() { - $order = array( 'excerpt', 'thumbnail' ); - set_theme_mod( 'neve_post_content_ordering', $order ); - - $this->assertSame( $order, $this->get_ordered_components() ); - $this->assertSame( $order, $this->get_ordered_components( true ) ); - } - - /** - * An array injected by a theme_mod_ filter is handled the same way. - */ - public function test_array_from_theme_mod_filter_does_not_fatal() { - $order = array( 'title-meta', 'excerpt' ); - $filter = function () use ( $order ) { - return $order; - }; - add_filter( 'theme_mod_neve_post_content_ordering', $filter ); - - $components = $this->get_ordered_components( true ); - - remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); - - $this->assertSame( $order, $components ); - } - - /** - * Values that are neither arrays nor valid JSON arrays fall back to the defaults. - * - * @param mixed $mod the stored mod value. - * - * @dataProvider provide_invalid_mods - */ - public function test_invalid_mod_falls_back_to_defaults( $mod ) { - set_theme_mod( 'neve_post_content_ordering', $mod ); - - $this->assertSame( $this->defaults, $this->get_ordered_components( true ) ); - } - - /** - * The customizer side reader tolerates the same shapes. - * - * @param mixed $mod the stored mod value. - * @param array $expected the expected order. - * - * @dataProvider provide_mod_shapes - */ - public function test_post_elements_order_handles_all_shapes( $mod, $expected ) { - set_theme_mod( 'neve_post_content_ordering', $mod ); - - $layout_blog = new \Neve\Customizer\Options\Layout_Blog(); - $method = new ReflectionMethod( $layout_blog, 'get_post_elements_order' ); - $method->setAccessible( true ); - - $this->assertSame( $expected, $method->invoke( $layout_blog ) ); - } - - /** - * Mod shapes and the order they should produce. - * - * @return array - */ - public function provide_mod_shapes() { - $defaults = array( 'thumbnail', 'title-meta', 'excerpt' ); - - return array( - 'json string' => array( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), array( 'excerpt', 'thumbnail' ) ), - 'array' => array( array( 'excerpt', 'thumbnail' ), array( 'excerpt', 'thumbnail' ) ), - 'broken json' => array( '[thumbnail,', $defaults ), - 'boolean' => array( true, $defaults ), - ); - } - - /** - * The sanitize callback keeps valid input and never fatals on an array. - */ - public function test_sanitize_handles_arrays_and_invalid_input() { - $layout_blog = new \Neve\Customizer\Options\Layout_Blog(); - $encoded = wp_json_encode( $this->defaults ); - - // A valid JSON string is passed through untouched. - $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( wp_json_encode( array( 'excerpt', 'thumbnail' ) ) ) ); - - // An array is accepted and normalized back to a JSON string. - $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( array( 'excerpt', 'thumbnail' ) ) ); - - // Associative, sparse and JSON object input is reindexed - the ordering control needs a JSON list. - $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( array( 'a' => 'excerpt', 'b' => 'thumbnail' ) ) ); - $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( array( 2 => 'excerpt', 5 => 'thumbnail' ) ) ); - $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( '{"a":"excerpt","b":"thumbnail"}' ) ); - - // Unknown components, broken JSON and scalars fall back to the defaults. - $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( wp_json_encode( array( 'thumbnail', 'evil' ) ) ) ); - $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( array( 'thumbnail', 'evil' ) ) ); - $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( '[thumbnail,' ) ); - $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( '' ) ); - $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( true ) ); - } - - /** - * Invalid mod values. - * - * @return array - */ - public function provide_invalid_mods() { - return array( - 'empty string' => array( '' ), - 'broken json' => array( '[thumbnail,' ), - 'json scalar' => array( '"thumbnail"' ), - 'integer' => array( 5 ), - 'boolean' => array( true ), - ); - } -} diff --git a/tests/test-neve-ordering.php b/tests/test-neve-ordering.php new file mode 100644 index 0000000000..7b46cc26ae --- /dev/null +++ b/tests/test-neve-ordering.php @@ -0,0 +1,372 @@ +get_ordered_components( $associative ); + } + + /** + * The default is used when no mod is set. + */ + public function test_blog_returns_defaults_when_mod_missing() { + $this->assertSame( $this->blog_defaults, $this->get_ordered_components() ); + $this->assertSame( $this->blog_defaults, $this->get_ordered_components( true ) ); + } + + /** + * A JSON string mod - the shape the Customizer control writes - is decoded. + */ + public function test_blog_decodes_json_string_mod() { + set_theme_mod( 'neve_post_content_ordering', wp_json_encode( array( 'title-meta', 'thumbnail' ) ) ); + + $this->assertSame( array( 'title-meta', 'thumbnail' ), $this->get_ordered_components( true ) ); + } + + /** + * An array-valued mod is returned as-is instead of fataling in json_decode(). + */ + public function test_blog_array_mod_does_not_fatal() { + $order = array( 'excerpt', 'thumbnail' ); + set_theme_mod( 'neve_post_content_ordering', $order ); + + $this->assertSame( $order, $this->get_ordered_components() ); + $this->assertSame( $order, $this->get_ordered_components( true ) ); + } + + /** + * An array injected by a theme_mod_ filter is handled the same way. + */ + public function test_blog_array_from_theme_mod_filter_does_not_fatal() { + $order = array( 'title-meta', 'excerpt' ); + $filter = function () use ( $order ) { + return $order; + }; + add_filter( 'theme_mod_neve_post_content_ordering', $filter ); + + $components = $this->get_ordered_components( true ); + + remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); + + $this->assertSame( $order, $components ); + } + + /** + * Values that are neither arrays nor valid JSON arrays fall back to the defaults. + * + * @param mixed $mod the stored mod value. + * + * @dataProvider provide_invalid_blog_mods + */ + public function test_blog_invalid_mod_falls_back_to_defaults( $mod ) { + set_theme_mod( 'neve_post_content_ordering', $mod ); + + $this->assertSame( $this->blog_defaults, $this->get_ordered_components( true ) ); + } + + /** + * Invalid blog mod values. + * + * @return array + */ + public function provide_invalid_blog_mods() { + return array( + 'empty string' => array( '' ), + 'broken json' => array( '[thumbnail,' ), + 'json scalar' => array( '"thumbnail"' ), + 'integer' => array( 5 ), + 'boolean' => array( true ), + ); + } + + /** + * The customizer side reader tolerates the same shapes. + * + * @param mixed $mod the stored mod value. + * @param array $expected the expected order. + * + * @dataProvider provide_blog_mod_shapes + */ + public function test_blog_post_elements_order_handles_all_shapes( $mod, $expected ) { + set_theme_mod( 'neve_post_content_ordering', $mod ); + + $layout_blog = new \Neve\Customizer\Options\Layout_Blog(); + $method = new ReflectionMethod( $layout_blog, 'get_post_elements_order' ); + $method->setAccessible( true ); + + $this->assertSame( $expected, $method->invoke( $layout_blog ) ); + } + + /** + * Blog mod shapes and the order they should produce. + * + * @return array + */ + public function provide_blog_mod_shapes() { + $defaults = array( 'thumbnail', 'title-meta', 'excerpt' ); + + return array( + 'json string' => array( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), array( 'excerpt', 'thumbnail' ) ), + 'array' => array( array( 'excerpt', 'thumbnail' ), array( 'excerpt', 'thumbnail' ) ), + 'broken json' => array( '[thumbnail,', $defaults ), + 'boolean' => array( true, $defaults ), + ); + } + + /** + * The blog sanitize callback keeps valid input and never fatals on an array. + */ + public function test_blog_sanitize_handles_arrays_and_invalid_input() { + $layout_blog = new \Neve\Customizer\Options\Layout_Blog(); + $encoded = wp_json_encode( $this->blog_defaults ); + + // A valid JSON string is passed through untouched. + $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( wp_json_encode( array( 'excerpt', 'thumbnail' ) ) ) ); + + // An array is accepted and normalized back to a JSON string. + $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( array( 'excerpt', 'thumbnail' ) ) ); + + // Associative, sparse and JSON object input is reindexed - the ordering control needs a JSON list. + $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( array( 'a' => 'excerpt', 'b' => 'thumbnail' ) ) ); + $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( array( 2 => 'excerpt', 5 => 'thumbnail' ) ) ); + $this->assertSame( wp_json_encode( array( 'excerpt', 'thumbnail' ) ), $layout_blog->sanitize_post_content_ordering( '{"a":"excerpt","b":"thumbnail"}' ) ); + + // Unknown components, broken JSON and scalars fall back to the defaults. + $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( wp_json_encode( array( 'thumbnail', 'evil' ) ) ) ); + $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( array( 'thumbnail', 'evil' ) ) ); + $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( '[thumbnail,' ) ); + $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( '' ) ); + $this->assertSame( $encoded, $layout_blog->sanitize_post_content_ordering( true ) ); + } + + /* + * --------------------------------------------------------------------------------------- + * Single post - neve_layout_single_post_elements_order + * --------------------------------------------------------------------------------------- + */ + + /** + * Get the rendering side content order. + * + * @return array + */ + private function get_content_order() { + $post_layout = new \Neve\Views\Post_Layout(); + $method = new ReflectionMethod( $post_layout, 'get_content_order' ); + $method->setAccessible( true ); + + return $method->invoke( $post_layout ); + } + + /** + * Ask the customizer whether an element is enabled. + * + * @param string $element the element slug. + * + * @return bool + */ + private function element_is_enabled( $element ) { + $layout_single_post = new \Neve\Customizer\Options\Layout_Single_Post(); + + return $layout_single_post->element_is_enabled( $element ); + } + + /** + * Run the single post control sanitize callback. + * + * @param mixed $value the value to sanitize. + * + * @return string + */ + private function sanitize_single_post( $value ) { + $layout_single_post = new \Neve\Customizer\Options\Layout_Single_Post(); + + return $layout_single_post->sanitize_post_elements_ordering( $value ); + } + + /** + * A JSON string mod - the shape the Customizer control writes - is decoded. + */ + public function test_single_post_decodes_json_string_mod() { + set_theme_mod( 'neve_layout_single_post_elements_order', wp_json_encode( array( 'content', 'tags' ) ) ); + + $this->assertSame( array( 'content', 'tags' ), $this->get_content_order() ); + $this->assertTrue( $this->element_is_enabled( 'tags' ) ); + $this->assertFalse( $this->element_is_enabled( 'comments' ) ); + } + + /** + * An array valued mod does not fatal on the rendering side. + */ + public function test_single_post_array_mod_does_not_fatal_on_render() { + set_theme_mod( 'neve_layout_single_post_elements_order', array( 'content', 'comments' ) ); + + $this->assertSame( array( 'content', 'comments' ), $this->get_content_order() ); + } + + /** + * An array valued mod does not fatal in the customizer active callback. + */ + public function test_single_post_array_mod_does_not_fatal_in_active_callback() { + set_theme_mod( 'neve_layout_single_post_elements_order', array( 'content', 'comments' ) ); + + $this->assertTrue( $this->element_is_enabled( 'comments' ) ); + $this->assertFalse( $this->element_is_enabled( 'tags' ) ); + } + + /** + * An array injected by a theme_mod_ filter - the shape Neve Pro uses for custom post + * types - is handled the same way. + */ + public function test_single_post_array_from_theme_mod_filter_does_not_fatal() { + $order = array( 'title-meta', 'content' ); + $filter = function () use ( $order ) { + return $order; + }; + add_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); + + $content_order = $this->get_content_order(); + $enabled = $this->element_is_enabled( 'content' ); + + remove_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); + + $this->assertSame( $order, $content_order ); + $this->assertTrue( $enabled ); + } + + /** + * Values that are neither arrays nor valid JSON arrays fall back to the defaults. + * + * @param mixed $mod the stored mod value. + * + * @dataProvider provide_invalid_single_post_mods + */ + public function test_single_post_invalid_mod_falls_back_to_defaults( $mod ) { + set_theme_mod( 'neve_layout_single_post_elements_order', $mod ); + + $this->assertContains( 'content', $this->get_content_order() ); + $this->assertTrue( $this->element_is_enabled( 'content' ) ); + } + + /** + * Invalid single post mod values. + * + * @return array + */ + public function provide_invalid_single_post_mods() { + return array( + 'empty string' => array( '' ), + 'broken json' => array( '[content,' ), + 'json scalar' => array( '"content"' ), + 'integer' => array( 5 ), + 'boolean' => array( true ), + ); + } + + /** + * The single post sanitize callback keeps valid input and never fatals on an array. + */ + public function test_single_post_sanitize_handles_arrays_and_invalid_input() { + $order = array( 'content', 'tags' ); + $encoded = wp_json_encode( $order ); + + // A valid JSON string is passed through. + $this->assertSame( $encoded, $this->sanitize_single_post( $encoded ) ); + + // An array is accepted and normalized back to a JSON string. + $this->assertSame( $encoded, $this->sanitize_single_post( $order ) ); + + // Associative, sparse and JSON object input is reindexed - the ordering control needs a JSON list. + $this->assertSame( $encoded, $this->sanitize_single_post( array( 'a' => 'content', 'b' => 'tags' ) ) ); + $this->assertSame( $encoded, $this->sanitize_single_post( array( 2 => 'content', 5 => 'tags' ) ) ); + $this->assertSame( $encoded, $this->sanitize_single_post( '{"a":"content","b":"tags"}' ) ); + + // Unknown components, broken JSON and scalars fall back to every allowed component. + foreach ( array( wp_json_encode( array( 'content', 'evil' ) ), array( 'content', 'evil' ), '[content,', '', true ) as $invalid ) { + $decoded = json_decode( $this->sanitize_single_post( $invalid ), true ); + $this->assertIsArray( $decoded ); + $this->assertContains( 'content', $decoded ); + $this->assertContains( 'comments', $decoded ); + } + } + + /** + * The metabox per post override tolerates an array or broken meta value. + */ + public function test_single_post_metabox_elements_order_filter_handles_all_shapes() { + $post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $this->go_to( get_permalink( $post_id ) ); + + $metabox = new \Neve\Views\Pluggable\Metabox_Settings(); + $fallback = array( 'title-meta', 'content' ); + + // A JSON string - the shape the editor sidebar saves - wins over the customizer order. + update_post_meta( $post_id, 'neve_post_elements_order', wp_json_encode( array( 'content', 'tags' ) ) ); + $this->assertSame( array( 'content', 'tags' ), $metabox->filter_post_elements( $fallback ) ); + + // An array valued meta - only reachable when the meta was written before the + // registered sanitize_text_field callback was in place - is returned as is + // instead of fataling. + add_filter( 'get_post_metadata', $array_meta = function ( $value, $object_id, $meta_key ) use ( $post_id ) { + if ( $object_id === $post_id && $meta_key === 'neve_post_elements_order' ) { + return array( array( 'tags', 'content' ) ); + } + + return $value; + }, 10, 3 ); + + $this->assertSame( array( 'tags', 'content' ), $metabox->filter_post_elements( $fallback ) ); + + remove_filter( 'get_post_metadata', $array_meta, 10 ); + + // Broken JSON falls back to the order passed in by the customizer. + update_post_meta( $post_id, 'neve_post_elements_order', '[content,' ); + $this->assertSame( $fallback, $metabox->filter_post_elements( $fallback ) ); + } +} From 212d6f73dbcf8017239b0eda569429553975f359 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Mon, 24 Aug 2026 15:27:45 +0530 Subject: [PATCH 2/4] fix: handle exceptions in component ordering --- tests/test-neve-ordering.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test-neve-ordering.php b/tests/test-neve-ordering.php index 7b46cc26ae..c6abb6677e 100644 --- a/tests/test-neve-ordering.php +++ b/tests/test-neve-ordering.php @@ -90,11 +90,12 @@ public function test_blog_array_from_theme_mod_filter_does_not_fatal() { }; add_filter( 'theme_mod_neve_post_content_ordering', $filter ); - $components = $this->get_ordered_components( true ); - - remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); - - $this->assertSame( $order, $components ); + try { + $components = $this->get_ordered_components( true ); + $this->assertSame( $order, $components ); + } finally { + remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); + } } /** @@ -107,6 +108,7 @@ public function test_blog_array_from_theme_mod_filter_does_not_fatal() { public function test_blog_invalid_mod_falls_back_to_defaults( $mod ) { set_theme_mod( 'neve_post_content_ordering', $mod ); + $this->assertSame( $this->blog_defaults, $this->get_ordered_components() ); $this->assertSame( $this->blog_defaults, $this->get_ordered_components( true ) ); } @@ -361,9 +363,11 @@ public function test_single_post_metabox_elements_order_filter_handles_all_shape return $value; }, 10, 3 ); - $this->assertSame( array( 'tags', 'content' ), $metabox->filter_post_elements( $fallback ) ); - - remove_filter( 'get_post_metadata', $array_meta, 10 ); + try { + $this->assertSame( array( 'tags', 'content' ), $metabox->filter_post_elements( $fallback ) ); + } finally { + remove_filter( 'get_post_metadata', $array_meta, 10 ); + } // Broken JSON falls back to the order passed in by the customizer. update_post_meta( $post_id, 'neve_post_elements_order', '[content,' ); From d2ffd667d1908b9a808d10af38ba8a1e289fcc07 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Mon, 24 Aug 2026 16:02:04 +0530 Subject: [PATCH 3/4] fix: improve element ordering validation --- inc/customizer/options/layout_single_post.php | 2 +- tests/test-neve-ordering.php | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/inc/customizer/options/layout_single_post.php b/inc/customizer/options/layout_single_post.php index 425b728efa..d12e5d292e 100644 --- a/inc/customizer/options/layout_single_post.php +++ b/inc/customizer/options/layout_single_post.php @@ -915,7 +915,7 @@ public function sanitize_post_elements_ordering( $value ) { $decoded = is_string( $value ) ? json_decode( $value, true ) : $value; - if ( ! is_array( $decoded ) || empty( $decoded ) ) { + if ( ! is_array( $decoded ) ) { return wp_json_encode( $allowed ); } diff --git a/tests/test-neve-ordering.php b/tests/test-neve-ordering.php index c6abb6677e..52054d542d 100644 --- a/tests/test-neve-ordering.php +++ b/tests/test-neve-ordering.php @@ -91,10 +91,10 @@ public function test_blog_array_from_theme_mod_filter_does_not_fatal() { add_filter( 'theme_mod_neve_post_content_ordering', $filter ); try { - $components = $this->get_ordered_components( true ); - $this->assertSame( $order, $components ); - } finally { - remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); + $components = $this->get_ordered_components( true ); + $this->assertSame( $order, $components ); + } finally { + remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); } } @@ -273,10 +273,12 @@ public function test_single_post_array_from_theme_mod_filter_does_not_fatal() { }; add_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); - $content_order = $this->get_content_order(); - $enabled = $this->element_is_enabled( 'content' ); - - remove_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); + try { + $content_order = $this->get_content_order(); + $enabled = $this->element_is_enabled( 'content' ); + } finally { + remove_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); + } $this->assertSame( $order, $content_order ); $this->assertTrue( $enabled ); @@ -364,9 +366,9 @@ public function test_single_post_metabox_elements_order_filter_handles_all_shape }, 10, 3 ); try { - $this->assertSame( array( 'tags', 'content' ), $metabox->filter_post_elements( $fallback ) ); - } finally { - remove_filter( 'get_post_metadata', $array_meta, 10 ); + $this->assertSame( array( 'tags', 'content' ), $metabox->filter_post_elements( $fallback ) ); + } finally { + remove_filter( 'get_post_metadata', $array_meta, 10 ); } // Broken JSON falls back to the order passed in by the customizer. From 68e72ff854c7c748c3627a2bd17f11e1d7b76a29 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Mon, 24 Aug 2026 16:50:51 +0530 Subject: [PATCH 4/4] fix: improve readability --- tests/test-neve-ordering.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test-neve-ordering.php b/tests/test-neve-ordering.php index 52054d542d..ab5ed3bcd1 100644 --- a/tests/test-neve-ordering.php +++ b/tests/test-neve-ordering.php @@ -95,7 +95,7 @@ public function test_blog_array_from_theme_mod_filter_does_not_fatal() { $this->assertSame( $order, $components ); } finally { remove_filter( 'theme_mod_neve_post_content_ordering', $filter ); - } + } } /** @@ -274,11 +274,11 @@ public function test_single_post_array_from_theme_mod_filter_does_not_fatal() { add_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); try { - $content_order = $this->get_content_order(); + $content_order = $this->get_content_order(); $enabled = $this->element_is_enabled( 'content' ); - } finally { - remove_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); - } + } finally { + remove_filter( 'theme_mod_neve_layout_single_post_elements_order', $filter ); + } $this->assertSame( $order, $content_order ); $this->assertTrue( $enabled ); @@ -357,19 +357,20 @@ public function test_single_post_metabox_elements_order_filter_handles_all_shape // An array valued meta - only reachable when the meta was written before the // registered sanitize_text_field callback was in place - is returned as is // instead of fataling. - add_filter( 'get_post_metadata', $array_meta = function ( $value, $object_id, $meta_key ) use ( $post_id ) { + $array_meta = function ( $value, $object_id, $meta_key ) use ( $post_id ) { if ( $object_id === $post_id && $meta_key === 'neve_post_elements_order' ) { return array( array( 'tags', 'content' ) ); } return $value; - }, 10, 3 ); + }; + add_filter( 'get_post_metadata', $array_meta, 10, 3 ); try { $this->assertSame( array( 'tags', 'content' ), $metabox->filter_post_elements( $fallback ) ); } finally { remove_filter( 'get_post_metadata', $array_meta, 10 ); - } + } // Broken JSON falls back to the order passed in by the customizer. update_post_meta( $post_id, 'neve_post_elements_order', '[content,' );