From 56fc6bdc3065016c12b0bb9cabcd52d597baeac5 Mon Sep 17 00:00:00 2001 From: Imants Date: Tue, 14 Jul 2026 13:14:38 +0300 Subject: [PATCH 01/47] fix: strip html tags from cloud snippet descriptions --- .../ManageMenu/CommunityCloud/CloudSnippetsTable.tsx | 4 ++-- src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx index 57a4939bc..e2d3f75ef 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx @@ -1,7 +1,7 @@ import { __ } from '@wordpress/i18n' import React, { useState } from 'react' import { getSnippetType } from '../../../utils/snippets/snippets' -import { truncateChars } from '../../../utils/text' +import { stripTags, truncateChars } from '../../../utils/text' import { Badge } from '../../common/Badge' import { Button } from '../../common/Button' import { CloudSnippetDownloadButton } from '../../common/cloud/CloudSnippetDownloadButton' @@ -36,7 +36,7 @@ const CloudSnippetRow: React.FC = ({ snippet }) => { -
{truncateChars(snippet.description)}
+
{truncateChars(stripTags(snippet.description))}
diff --git a/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx b/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx index c2f040029..deeecc067 100644 --- a/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { __, _x, sprintf } from '@wordpress/i18n' import { getSnippetType } from '../../../utils/snippets/snippets' -import { truncateChars } from '../../../utils/text' +import { stripTags, truncateChars } from '../../../utils/text' import { Badge } from '../../common/Badge' import { Button } from '../../common/Button' import { CloudSnippetDownloadButton } from '../../common/cloud/CloudSnippetDownloadButton' @@ -50,7 +50,7 @@ const CloudSnippetDetails: React.FC = ({ snippet, setI {snippet.description && (

- {truncateChars(snippet.description)} + {truncateChars(stripTags(snippet.description))}

)} From fb378a7462eef5b87d17021b5dc5f5694a381c58 Mon Sep 17 00:00:00 2001 From: Imants Date: Tue, 14 Jul 2026 13:14:47 +0300 Subject: [PATCH 02/47] fix: filter foreign admin notices on code snippets screens --- src/css/common/_subnav.scss | 6 - src/php/Admin/Bootstrap_Admin.php | 8 ++ src/php/Admin/Menus/Admin_Menu.php | 9 ++ src/php/Admin/Notice_Filter.php | 157 ++++++++++++++++++++++++ tests/unit/Admin/Notice_Filter_Test.php | 119 ++++++++++++++++++ 5 files changed, 293 insertions(+), 6 deletions(-) create mode 100644 src/php/Admin/Notice_Filter.php create mode 100644 tests/unit/Admin/Notice_Filter_Test.php diff --git a/src/css/common/_subnav.scss b/src/css/common/_subnav.scss index d29852ca8..e3416243a 100644 --- a/src/css/common/_subnav.scss +++ b/src/css/common/_subnav.scss @@ -150,9 +150,3 @@ background: #dbebf7; color: #2271b1; } - -// Add a proper separation between the navigation and any admin notices that are displayed above it. -.notice + .wrap .snippet-type-nav { - margin-block-start: 20px; - border-block-start: 1px solid #c3c4c7; -} diff --git a/src/php/Admin/Bootstrap_Admin.php b/src/php/Admin/Bootstrap_Admin.php index 2e4168ee9..194cabab4 100644 --- a/src/php/Admin/Bootstrap_Admin.php +++ b/src/php/Admin/Bootstrap_Admin.php @@ -38,6 +38,13 @@ class Bootstrap_Admin { */ public Welcome_Client $welcome_client; + /** + * Notice_Filter class instance. + * + * @var Notice_Filter + */ + public Notice_Filter $notice_filter; + /** * Class constructor. */ @@ -47,6 +54,7 @@ public function __construct() { } $this->welcome_client = new Welcome_Client(); + $this->notice_filter = new Notice_Filter(); add_action( 'init', array( $this, 'load_classes' ), 11 ); diff --git a/src/php/Admin/Menus/Admin_Menu.php b/src/php/Admin/Menus/Admin_Menu.php index dc96c1219..e5a20db13 100644 --- a/src/php/Admin/Menus/Admin_Menu.php +++ b/src/php/Admin/Menus/Admin_Menu.php @@ -119,6 +119,15 @@ public function register() { $this->add_menu( $this->slug, $this->label, $this->title ); } + /** + * Retrieve the WordPress hookname computed for this menu's admin page. + * + * @return string + */ + public function get_hookname(): string { + return get_plugin_page_hookname( $this->slug, $this->base_slug ); + } + /** * Render the navigation bar at the top of the admin page. */ diff --git a/src/php/Admin/Notice_Filter.php b/src/php/Admin/Notice_Filter.php new file mode 100644 index 000000000..010377cab --- /dev/null +++ b/src/php/Admin/Notice_Filter.php @@ -0,0 +1,157 @@ +is_code_snippets_screen( $screen ) ) { + return; + } + + if ( ! apply_filters( 'code_snippets/admin/filter_foreign_notices', true ) ) { + return; + } + + add_action( 'admin_head', [ $this, 'filter_foreign_notices' ], 0 ); + add_action( 'admin_head', [ $this, 'print_fallback_styles' ] ); + } + + /** + * Remove every notice callback that is not defined within the Code Snippets plugin directory. + * + * @return void + */ + public function filter_foreign_notices() { + global $wp_filter; + + foreach ( self::NOTICE_HOOKS as $hook ) { + if ( empty( $wp_filter[ $hook ] ) ) { + continue; + } + + foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) { + foreach ( $callbacks as $callback ) { + if ( ! $this->is_code_snippets_callback( $callback['function'] ) ) { + remove_action( $hook, $callback['function'], $priority ); + } + } + } + } + } + + /** + * Print inline styles that hide any residual foreign notices left in the notice region. + * + * @return void + */ + public function print_fallback_styles() { + ?> + + admin ) ) { + return false; + } + + foreach ( code_snippets()->admin->menus as $menu ) { + $hookname = $menu->get_hookname(); + + foreach ( [ $hookname, $hookname . '-network' ] as $candidate ) { + if ( $screen->id === $candidate || $screen->base === $candidate ) { + return true; + } + } + } + + return false; + } + + /** + * Determine whether a callback is defined within the Code Snippets plugin directory. + * + * Ownership is resolved from the callback's defining file. Reflection failures are treated as + * Code Snippets-owned so unknown callbacks are never removed. + * + * @param callable|string|array $callback Hook callback as stored in the filter registry. + * + * @return bool + */ + private function is_code_snippets_callback( $callback ): bool { + try { + if ( is_array( $callback ) ) { + $file = ( new ReflectionMethod( $callback[0], $callback[1] ) )->getFileName(); + } elseif ( is_string( $callback ) && false !== strpos( $callback, '::' ) ) { + [ $class, $method ] = explode( '::', $callback, 2 ); + $file = ( new ReflectionMethod( $class, $method ) )->getFileName(); + } else { + $file = ( new ReflectionFunction( $callback ) )->getFileName(); + } + } catch ( Throwable $error ) { + return true; + } + + if ( ! $file ) { + return true; + } + + return 0 === strpos( $file, dirname( PLUGIN_FILE ) ); + } +} diff --git a/tests/unit/Admin/Notice_Filter_Test.php b/tests/unit/Admin/Notice_Filter_Test.php new file mode 100644 index 000000000..0d2455678 --- /dev/null +++ b/tests/unit/Admin/Notice_Filter_Test.php @@ -0,0 +1,119 @@ +user->create( [ 'role' => 'administrator' ] ); + } + + /** + * Set up before each test. + * + * @return void + */ + public function set_up() { + parent::set_up(); + + wp_set_current_user( self::$admin_user_id ); + set_current_screen( 'toplevel_page_' . code_snippets()->get_menu_slug() ); + + if ( ! isset( code_snippets()->admin ) ) { + code_snippets()->admin = new Bootstrap_Admin(); + code_snippets()->admin->load_classes(); + } + + $this->notice_filter = new Notice_Filter(); + } + + /** + * Filtering removes foreign notice callbacks while keeping Code Snippets-owned callbacks. + * + * @return void + */ + public function test_filtering_removes_foreign_notices_and_keeps_code_snippets_notices(): void { + $foreign = static function () {}; + $owned = [ code_snippets()->admin, 'print_notices' ]; + + add_action( 'admin_notices', $foreign ); + add_action( 'admin_notices', $owned ); + + $this->notice_filter->filter_foreign_notices(); + + $this->assertFalse( has_action( 'admin_notices', $foreign ) ); + $this->assertNotFalse( has_action( 'admin_notices', $owned ) ); + } + + /** + * A Code Snippets screen registers the filtering hooks by default. + * + * @return void + */ + public function test_registers_filtering_on_code_snippets_screen(): void { + $this->notice_filter->register_filtering( get_current_screen() ); + + $this->assertNotFalse( has_action( 'admin_head', [ $this->notice_filter, 'filter_foreign_notices' ] ) ); + $this->assertNotFalse( has_action( 'admin_head', [ $this->notice_filter, 'print_fallback_styles' ] ) ); + } + + /** + * The filter_foreign_notices filter disables filtering when set to false. + * + * @return void + */ + public function test_filter_disables_filtering(): void { + add_filter( 'code_snippets/admin/filter_foreign_notices', '__return_false' ); + + $this->notice_filter->register_filtering( get_current_screen() ); + + $this->assertFalse( has_action( 'admin_head', [ $this->notice_filter, 'filter_foreign_notices' ] ) ); + } + + /** + * Non-Code Snippets screens do not register filtering hooks. + * + * @return void + */ + public function test_does_not_register_on_foreign_screen(): void { + set_current_screen( 'dashboard' ); + + $this->notice_filter->register_filtering( get_current_screen() ); + + $this->assertFalse( has_action( 'admin_head', [ $this->notice_filter, 'filter_foreign_notices' ] ) ); + } +} From d3b8440444adf85eb1ae6ad386a24ec63c90a7b2 Mon Sep 17 00:00:00 2001 From: Imants Date: Tue, 14 Jul 2026 13:14:57 +0300 Subject: [PATCH 03/47] fix: improve snippet preview modal sizing and theme --- src/css/common/_modal.scss | 15 +++++ src/css/manage.scss | 1 + .../components/common/SnippetPreviewModal.tsx | 65 ++++++++++++++----- src/js/types/Window.ts | 1 + src/php/Admin/Menus/Manage_Menu.php | 6 ++ 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/css/common/_modal.scss b/src/css/common/_modal.scss index dab7cca7b..2c65bc274 100644 --- a/src/css/common/_modal.scss +++ b/src/css/common/_modal.scss @@ -51,3 +51,18 @@ } } } + +.components-modal__frame.code-snippets-preview-modal { + min-inline-size: 520px; + min-block-size: 240px; + max-inline-size: 80vw; + max-block-size: 80vh; + + @media (width <= 600px) { + min-inline-size: 90vw; + } + + .components-modal__content { + overflow: auto; + } +} diff --git a/src/css/manage.scss b/src/css/manage.scss index 8b7183fc0..163624485 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -4,6 +4,7 @@ @use 'common/tooltips'; @use 'common/direction'; @use 'common/select'; +@use 'common/modal'; @use 'common/notices'; @use 'common/upsell'; @use 'common/toolbar'; diff --git a/src/js/components/common/SnippetPreviewModal.tsx b/src/js/components/common/SnippetPreviewModal.tsx index fa8369912..d208a5c60 100644 --- a/src/js/components/common/SnippetPreviewModal.tsx +++ b/src/js/components/common/SnippetPreviewModal.tsx @@ -1,6 +1,7 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useRef } from 'react' import { Modal } from '@wordpress/components' -import { Prism } from '../../utils/Prism' +import { __ } from '@wordpress/i18n' +import type { EditorFromTextArea } from 'codemirror' export interface SnippetPreviewModalProps { title: string @@ -10,26 +11,58 @@ export interface SnippetPreviewModalProps { setIsOpen: (isOpen: boolean) => void } +// Mirrors the type-to-mode mapping used by the live editor in SnippetTypeInput. +const EDITOR_MODES: Record = { + css: 'text/css', + js: 'javascript', + php: 'text/x-php', + html: 'application/x-httpd-php' +} + /** - * Modal for quickly viewing a snippet's code with syntax highlighting, - * without navigating to the edit page. Shared between local snippets and - * cloud snippet previews. + * Modal for quickly viewing a snippet's code in a read-only CodeMirror editor, + * without navigating to the edit page. Shared between local snippets and cloud + * snippet previews. */ -export const SnippetPreviewModal: React.FC = ({ title, code, type, isOpen, setIsOpen }) => { +export const SnippetPreviewModal: React.FC = ({ + title, + code, + type, + isOpen, + setIsOpen +}) => { + const textareaRef = useRef(null) + useEffect(() => { - if (isOpen) { - Prism.highlightAll() + if (!isOpen || !textareaRef.current) { + return undefined + } + + const instance = window.wp.codeEditor.initialize(textareaRef.current, { + codemirror: { + readOnly: true, + theme: window.CODE_SNIPPETS_MANAGE?.editorTheme ?? 'default', + mode: EDITOR_MODES[type] ?? EDITOR_MODES.php + } + }) + + return () => { + (instance.codemirror as EditorFromTextArea).toTextArea() } - }, [isOpen]) + }, [isOpen, type]) return isOpen - ? setIsOpen(false)} title={title}> -
-				
-					{'php' === type ? '
-			
+ ? setIsOpen(false)} + title={title} + > +