From abd6bdaf4b773a90496dec870e07be4fa6834db7 Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:21:42 +0100 Subject: [PATCH 1/7] fix: match import type badges with their own cell instead of a doubled selector --- src/css/import/_upload.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/css/import/_upload.scss b/src/css/import/_upload.scss index 7183de6d5..ffaac663b 100644 --- a/src/css/import/_upload.scss +++ b/src/css/import/_upload.scss @@ -321,15 +321,15 @@ text-transform: uppercase; border-radius: 5px; - @at-root .import-select-card .html-snippet & { + @at-root .import-select-card .html-snippet .column-type span { background-color: #cd4510; } - @at-root .import-select-card .js-snippet & { + @at-root .import-select-card .js-snippet .column-type span { background-color: #f7d67a; } - @at-root .import-select-card .css-snippet & { + @at-root .import-select-card .css-snippet .column-type span { background-color: #9b59b6; } } From 8748668423012a30872854b9a706942a1e1a8be7 Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:21:44 +0100 Subject: [PATCH 2/7] fix: rebuild the Run Once link from the refreshed nonce before any navigation --- .../components/ManageMenu/SnippetsTable/TableColumns.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx b/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx index 53e3c0dd1..f3329e56c 100644 --- a/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx @@ -31,13 +31,18 @@ const runOnceUrl = (snippet: Snippet, nonce: string): string => _wpnonce: nonce }) -// The rendered link carries the nonce from page load; the click reads the one -// the Heartbeat has refreshed since, so a page left open still works. +// The rendered link carries the nonce from page load. Before any navigation +// starts, whether a click, a middle-click or "open in new tab", the href is +// rebuilt from the nonce the Heartbeat has refreshed since, so a page left open +// still works. const RunOnceButton: React.FC = ({ snippet }) => { + event.currentTarget.href = runOnceUrl(snippet, getRunOnceNonce()) + }} onClick={event => { event.preventDefault() window.location.assign(runOnceUrl(snippet, getRunOnceNonce())) From ff5767a61a358bc3c8a85a295866c192a9b124c3 Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:21:55 +0100 Subject: [PATCH 3/7] fix: report the edit page's real hookname now it has no parent menu --- src/php/Admin/Menus/Edit_Menu.php | 18 +++++++++++++++--- tests/unit/Admin/Menus/Edit_Menu_Test.php | 19 ++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/php/Admin/Menus/Edit_Menu.php b/src/php/Admin/Menus/Edit_Menu.php index b8f5057de..1effc2ccf 100644 --- a/src/php/Admin/Menus/Edit_Menu.php +++ b/src/php/Admin/Menus/Edit_Menu.php @@ -148,6 +148,19 @@ private function add_current_snippet_menu_item( int $snippet_id ): void { ); } + /** + * Retrieve the hookname of the edit page. + * + * The page is registered without a parent, so WordPress files it under + * "admin_page_" rather than under the Snippets menu; deriving it from the + * menu slug would name a screen that does not exist. + * + * @return string + */ + public function get_hookname(): string { + return get_plugin_page_hookname( $this->slug, '' ); + } + /** * Retrieve every hookname registered by this menu, including the separate * "Add New" page, so screen-based checks recognise both editor views. @@ -182,9 +195,8 @@ public function load() { * @return void */ protected function ensure_correct_page() { - $screen = get_current_screen(); - $edit_hook = get_plugin_page_hookname( $this->slug, $this->base_slug ); - $edit_hook .= $screen->in_admin( 'network' ) ? '-network' : ''; + $screen = get_current_screen(); + $edit_hook = $this->get_hookname() . ( $screen->in_admin( 'network' ) ? '-network' : '' ); // Disallow visiting the edit snippet page without a valid ID. if ( diff --git a/tests/unit/Admin/Menus/Edit_Menu_Test.php b/tests/unit/Admin/Menus/Edit_Menu_Test.php index 7b9ec3f9e..bfa981ed2 100644 --- a/tests/unit/Admin/Menus/Edit_Menu_Test.php +++ b/tests/unit/Admin/Menus/Edit_Menu_Test.php @@ -21,7 +21,8 @@ public function set_up() { parent::set_up(); set_current_screen( 'toplevel_page_' . code_snippets()->get_menu_slug() ); - unset( $GLOBALS['submenu'][ code_snippets()->get_menu_slug() ] ); + // The hidden page's registration must be proven by each test, not inherited. + unset( $GLOBALS['submenu'][ code_snippets()->get_menu_slug() ], $GLOBALS['submenu'][''] ); unset( $_GET['id'] ); } @@ -113,4 +114,20 @@ public function test_edit_menu_does_not_use_footer_inline_script(): void { $this->assertFalse( has_action( 'admin_print_footer_scripts', [ $menu, 'disable_menu_link' ] ) ); $this->assertFalse( has_action( 'network_admin_print_footer_scripts', [ $menu, 'disable_menu_link' ] ) ); } + + /** + * The hookname the menu reports is the one WordPress registered for the parentless page. + * + * @return void + */ + public function test_hookname_matches_the_registered_page(): void { + $menu = new Edit_Menu(); + $menu->register(); + + $registered = get_plugin_page_hookname( code_snippets()->get_menu_slug( 'edit' ), '' ); + + $this->assertSame( $registered, $menu->get_hookname() ); + $this->assertContains( $registered, $menu->get_hooknames() ); + $this->assertNotFalse( has_action( 'load-' . $registered, [ $menu, 'load' ] ), 'the load hook is bound to the same name' ); + } } From 6efc2ec9b24df858e31ab7da36abbaca20e47f86 Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:22:02 +0100 Subject: [PATCH 4/7] fix: only strip a closing code fence when an opening one was removed --- src/php/snippet-ops.php | 33 ++++++++++++++++--- .../Snippets/Normalize_Snippet_Code_Test.php | 2 ++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 9b073638e..7023ce63a 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -101,6 +101,22 @@ function clean_snippets_cache( string $table_name ) { * @return bool Whether the group was flushed. */ function flush_cache_group( string $group ): bool { + /** + * Short-circuits flushing a cache group. + * + * Returning a boolean skips the object cache entirely: false makes the + * caller fall back to deleting the known keys one by one, for a cache + * that reports group support it does not really have. + * + * @param bool|null $flushed Whether the group was flushed, or null to let the cache try. + * @param string $group Cache group. + */ + $flushed = apply_filters( 'code_snippets/pre_flush_cache_group', null, $group ); + + if ( null !== $flushed ) { + return (bool) $flushed; + } + if ( ! function_exists( 'wp_cache_flush_group' ) || ! function_exists( 'wp_cache_supports' ) || ! wp_cache_supports( 'flush_group' ) ) { @@ -142,10 +158,12 @@ function flush_versioned_cache_groups( string $previous_version ): void { * @return void */ function flush_known_cache_keys(): void { - clean_snippets_cache( code_snippets()->db->get_table_name( false ) ); + // Both tables' keys go, whether or not this is a network: deleting a key + // that was never written costs nothing, and it keeps one path to test. + $tables = [ code_snippets()->db->get_table_name( false ), code_snippets()->db->get_table_name( true ) ]; - if ( is_multisite() ) { - clean_snippets_cache( code_snippets()->db->get_table_name( true ) ); + foreach ( array_unique( $tables ) as $table ) { + clean_snippets_cache( $table ); } wp_cache_delete( Settings\CACHE_KEY, CACHE_GROUP ); @@ -914,8 +932,13 @@ function get_snippet_by_cloud_id( string $cloud_id, ?bool $multisite = null ): ? */ function normalize_snippet_code( string $code, string $type ): string { // A markdown fence around the whole snippet, as copied from a chat window. - $code = preg_replace( '/\A\s*```[a-z]*[ \t]*\R/i', '', $code ); - $code = preg_replace( '/\R\s*```\s*\z/', '', $code ); + // The closing fence only goes when an opening one was there: on its own it + // is the author's content, as in an HTML snippet ending in backticks. + $code = preg_replace( '/\A\s*```[a-z]*[ \t]*\R/i', '', $code, 1, $fenced ); + + if ( $fenced ) { + $code = preg_replace( '/\R\s*```\s*\z/', '', $code ); + } switch ( $type ) { case 'php': diff --git a/tests/unit/Snippets/Normalize_Snippet_Code_Test.php b/tests/unit/Snippets/Normalize_Snippet_Code_Test.php index 7d9a17b69..b2fb5c1c2 100644 --- a/tests/unit/Snippets/Normalize_Snippet_Code_Test.php +++ b/tests/unit/Snippets/Normalize_Snippet_Code_Test.php @@ -78,6 +78,8 @@ public function untouched_code_provider(): array { 'script tag inside php' => [ 'php', "echo '';" ], 'style tag mid-css' => [ 'css', ".a { content: '" ], ]; } From 29ca13f4b0774ed75be9209020d48fe00e6a1626 Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:22:09 +0100 Subject: [PATCH 5/7] test: cover the cache flush fallback and both snippet tables --- tests/unit/Core/Versioned_Cache_Test.php | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/unit/Core/Versioned_Cache_Test.php b/tests/unit/Core/Versioned_Cache_Test.php index acbd30e36..21f61e158 100644 --- a/tests/unit/Core/Versioned_Cache_Test.php +++ b/tests/unit/Core/Versioned_Cache_Test.php @@ -115,11 +115,14 @@ public function test_empty_previous_version_is_tolerated(): void { * @return void */ public function test_known_keys_are_deleted_without_a_group_flush(): void { - $table = code_snippets()->db->get_table_name( false ); - $keys = [ + $table = code_snippets()->db->get_table_name( false ); + $network = code_snippets()->db->get_table_name( true ); + $keys = [ "all_snippets_$table", "all_snippet_tags_$table", 'active_snippets_global_single-use_front-end_' . $table, + "all_snippets_$network", + "all_snippet_tags_$network", \Code_Snippets\Settings\CACHE_KEY, ]; @@ -147,4 +150,26 @@ public function test_versioned_flush_leaves_no_snippet_data(): void { $this->assertFalse( wp_cache_get( "all_snippets_$table", CACHE_GROUP ) ); } + + /** + * When the cache cannot flush a group, the full flush still removes every known key. + * + * @return void + */ + public function test_versioned_flush_falls_back_to_known_keys(): void { + $table = code_snippets()->db->get_table_name( false ); + $keys = [ "all_snippets_$table", "all_snippet_tags_$table", \Code_Snippets\Settings\CACHE_KEY ]; + + foreach ( $keys as $key ) { + wp_cache_set( $key, 'stale', CACHE_GROUP ); + } + + add_filter( 'code_snippets/pre_flush_cache_group', '__return_false' ); + flush_versioned_cache_groups( '' ); + remove_filter( 'code_snippets/pre_flush_cache_group', '__return_false' ); + + foreach ( $keys as $key ) { + $this->assertFalse( wp_cache_get( $key, CACHE_GROUP ), $key ); + } + } } From 7e8eb978e21bd61ff4e36a2cbc0da4fcf2952314 Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:22:16 +0100 Subject: [PATCH 6/7] test: cover the settings layout, headings and current section --- tests/unit/Settings/Settings_Layout_Test.php | 144 +++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/unit/Settings/Settings_Layout_Test.php diff --git a/tests/unit/Settings/Settings_Layout_Test.php b/tests/unit/Settings/Settings_Layout_Test.php new file mode 100644 index 000000000..3d9f9547f --- /dev/null +++ b/tests/unit/Settings/Settings_Layout_Test.php @@ -0,0 +1,144 @@ +assertNotContains( [ 'general', 'no_such_field' ], $shown, 'a field with no definition is skipped' ); + $this->assertNotContains( [ 'general', 'admin_bar_snippet_limit' ], $hidden, 'a field whose condition is not met is skipped' ); + $this->assertContains( [ 'general', 'admin_bar_snippet_limit' ], $shown ); + $this->assertContains( [ 'general', 'enable_admin_bar' ], $hidden, 'the field the condition depends on is always there' ); + $this->assertSame( [], Settings_Layout::get_visible_fields( 'no-such-tab', $settings ) ); + } + + /** + * A tab with nothing to show is not offered. + * + * @return void + */ + public function test_tabs_with_nothing_to_show_are_unavailable(): void { + add_filter( + 'code_snippets_settings_tabs', + static function ( array $tabs ): array { + $tabs['empty'] = 'Empty'; + return $tabs; + } + ); + + $available = Settings_Layout::get_available_tabs(); + + $this->assertArrayHasKey( 'editing', $available ); + $this->assertArrayNotHasKey( 'empty', $available ); + } + + /** + * A group heading renders once for its group, escaped, and a labelable field gets a real label. + * + * @return void + */ + public function test_group_headings_render_once_and_escaped(): void { + add_settings_section( 'layout-test', 'Layout test', '__return_empty_string', Settings_Menu::SETTINGS_PAGE ); + add_settings_field( 'first', 'First', '__return_null', Settings_Menu::SETTINGS_PAGE, 'layout-test', [ 'group_heading' => 'Group one' ] ); + add_settings_field( + 'second', + 'Second', + '__return_null', + Settings_Menu::SETTINGS_PAGE, + 'layout-test', + [ + 'group_heading' => 'Group one', + 'label_for' => 'field-second', + ] + ); + add_settings_field( 'third', 'Third', '__return_null', Settings_Menu::SETTINGS_PAGE, 'layout-test', [ 'group_heading' => 'Group two' ] ); + + ob_start(); + do_settings_fields_with_headings( Settings_Menu::SETTINGS_PAGE, 'layout-test' ); + $html = (string) ob_get_clean(); + + $this->assertSame( 1, substr_count( $html, 'Group <b>one</b>' ), 'the heading is drawn once and escaped' ); + $this->assertStringNotContainsString( 'one', $html ); + $this->assertStringContainsString( 'Group two', $html ); + $this->assertStringContainsString( '', $html ); + $this->assertStringContainsString( 'First', $html ); + } + + /** + * The current section is the requested one when it exists, else the default, else the first. + * + * @return void + */ + public function test_current_section_falls_back_sensibly(): void { + global $wp_settings_sections; + + // Only the registered sections are read, so the menu's dependencies are not needed. + $menu = ( new ReflectionClass( Settings_Menu::class ) )->newInstanceWithoutConstructor(); + + $this->assertSame( 'anything', $menu->get_current_section( 'anything' ), 'with no sections the default is returned as given' ); + + $wp_settings_sections[ Settings_Menu::SETTINGS_PAGE ] = [ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- test fixture, removed in tear_down. + 'editing' => [ 'id' => 'editing' ], + 'running' => [ 'id' => 'running' ], + ]; + + $this->assertSame( 'running', $menu->get_current_section( 'running' ) ); + $this->assertSame( 'editing', $menu->get_current_section( 'no-such-section' ), 'an unknown default falls back to the first tab' ); + + $_REQUEST['section'] = 'running'; + $this->assertSame( 'running', $menu->get_current_section() ); + + $_REQUEST['section'] = ''; + $this->assertSame( 'editing', $menu->get_current_section(), 'an invalid request value falls back to the first tab' ); + } +} From 73a8c61427283d73d88268bee8da3fbc7323c74b Mon Sep 17 00:00:00 2001 From: TallblokeUK Date: Thu, 3 Sep 2026 22:22:23 +0100 Subject: [PATCH 7/7] test: require the capability for Run Once and assert the fixture saved --- .../Manage/Manage_Menu_Run_Once_Test.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Run_Once_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Run_Once_Test.php index c7e60c117..80ef1e21a 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Run_Once_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Run_Once_Test.php @@ -101,7 +101,10 @@ private function single_use( string $code ): Snippet { $snippet->code = $code; $snippet->active = false; - return save_snippet( $snippet ); + $saved = save_snippet( $snippet ); + $this->assertNotNull( $saved, 'the single-use snippet must save before the test can run it' ); + + return $saved; } /** @@ -187,4 +190,20 @@ public function test_heartbeat_refreshes_the_nonce(): void { wp_set_current_user( self::factory()->user->create( [ 'role' => 'subscriber' ] ) ); $this->assertArrayNotHasKey( 'code_snippets_run_once_nonce', $menu->refresh_run_once_nonce( [] ) ); } + + /** + * A user without the capability is refused even with a nonce of their own. + * + * @return void + */ + public function test_capability_is_required_even_with_a_valid_nonce(): void { + $snippet = $this->single_use( 'update_option( "run_once_ran", "yes" );' ); + + wp_set_current_user( self::factory()->user->create( [ 'role' => 'subscriber' ] ) ); + $own_nonce = wp_create_nonce( Manage_Menu::RUN_ONCE_NONCE ); + + $this->assertNull( $this->run_once_request( $snippet->id, $own_nonce ) ); + $this->assertFalse( (bool) get_snippet( $snippet->id )->active ); + $this->assertFalse( get_option( 'run_once_ran' ) ); + } }