diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index ff7bf39b4..30e5bf94b 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,14 +1,14 @@
blank_issues_enabled: true
contact_links:
- name: ❓ Support Question
- url: https://help.codesnippets.pro/
+ url: https://codesnippets.pro/support/
about: For Code Snippets users who need assistant and have general usage questions.
- name: 💎 Premium Support - For customers only
- url: https://help.codesnippets.pro/
+ url: https://codesnippets.pro/support/
about: If you have an active license you are entitled to premium support.
- name: 🗨️ Code Snippets Facebook Community
url: https://www.facebook.com/groups/codesnippetsplugin
about: The main Facebook group where all kinds of users come together to help each other.
- name: 📚 Code Snippets Documentation
- url: https://help.codesnippets.pro/
+ url: https://codesnippets.pro/docs/
about: Anything you need to know about Code Snippets and what to do if you may have an issue.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e7d50cbd..50ab5193e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [3.9.4] (2026-01-14)
+
+### Added
+* New import functionality to migrate snippets from file uploads with drag-and-drop interface
+* Support for importing snippets from other popular plugins (Header Footer Code Manager, Insert Headers and Footers, Insert PHP Code Snippet)
+* Enhanced file based execution support with improved multisite mode compatibility
+
+### Changed
+* Updated links to more recent documentation pages
+
+### Fixed
+* Fixed multisite capability checks in Plugin class
+* Fixed snippet execution logic for multisite support by centralizing trashed snippet handling
+* Fixed multisite snippet handling to ensure local snippets use correct table and filter out trashed snippets
## [3.9.3] (2025-12-03)
@@ -378,7 +392,7 @@
### Added
* Added additional editor shortcuts to list in tooltip.
-* Filter for changing Snippets admin menu position. [See this help article for more information.](https://help.codesnippets.pro/article/61-how-can-i-change-the-location-of-the-snippets-admin-menu)
+* Filter for changing Snippets admin menu position. [See this help article for more information.](https://codesnippets.pro/doc/snippets-menu-location/)
* Ability to filter shortcode output. Thanks to contributions from [Jack Szwergold](https://github.com/JackSzwergold).
### Fixed
diff --git a/README.md b/README.md
index 931134db4..6a8240fd3 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,6 @@ WordPress plugin for managing executable code snippets through a graphical inter
- [Read more on WordPress.org](https://wordpress.org/plugins/code-snippets)
- [Download the latest stable version](https://downloads.wordpress.org/plugin/code-snippets.latest-stable.zip)
-- [Read the help documentation](https://help.codesnippets.pro)
+- [Read the help documentation](https://codesnippets.pro/docs/)
- [Leave a review](https://wordpress.org/support/plugin/code-snippets/reviews/#new-post)
- [Translate into your language](https://translate.wordpress.org/projects/wp-plugins/code-snippets/)
diff --git a/config/webpack-js.ts b/config/webpack-js.ts
index 3e2a19ce8..5d50210d6 100644
--- a/config/webpack-js.ts
+++ b/config/webpack-js.ts
@@ -24,13 +24,14 @@ const babelConfig = {
export const jsWebpackConfig: Configuration = {
entry: {
- edit: { import: `${SOURCE_DIR}/edit.tsx`, dependOn: 'editor' },
- editor: `${SOURCE_DIR}/editor.ts`,
- import: `${SOURCE_DIR}/import.tsx`,
- manage: `${SOURCE_DIR}/manage.ts`,
- mce: `${SOURCE_DIR}/mce.ts`,
- prism: `${SOURCE_DIR}/prism.ts`,
- settings: { import: `${SOURCE_DIR}/settings.ts`, dependOn: 'editor' }
+ 'admin-bar': `${SOURCE_DIR}/admin-bar.ts`,
+ 'edit': { import: `${SOURCE_DIR}/edit.tsx`, dependOn: 'editor' },
+ 'editor': `${SOURCE_DIR}/editor.ts`,
+ 'import': `${SOURCE_DIR}/import.tsx`,
+ 'manage': `${SOURCE_DIR}/manage.ts`,
+ 'mce': `${SOURCE_DIR}/mce.ts`,
+ 'prism': `${SOURCE_DIR}/prism.ts`,
+ 'settings': { import: `${SOURCE_DIR}/settings.ts`, dependOn: 'editor' }
},
output: {
path: join(resolve(__dirname), '..', DEST_DIR),
diff --git a/package-lock.json b/package-lock.json
index 3260a8438..72a75d7b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "code-snippets",
- "version": "3.9.3",
+ "version": "3.9.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "code-snippets",
- "version": "3.9.3",
+ "version": "3.9.4",
"license": "GPL-2.0-or-later",
"dependencies": {
"@codemirror/fold": "^0.19.4",
diff --git a/package.json b/package.json
index ff50f0f85..6ff420068 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "code-snippets",
"description": "Manage code snippets running on a WordPress-powered site through a graphical interface.",
"homepage": "https://codesnippets.pro",
- "version": "3.9.3",
+ "version": "3.9.4",
"main": "src/dist/edit.js",
"directories": {
"test": "tests"
diff --git a/src/code-snippets.php b/src/code-snippets.php
index ad9b318f5..c1ce0349f 100644
--- a/src/code-snippets.php
+++ b/src/code-snippets.php
@@ -8,11 +8,11 @@
* License: GPL-2.0-or-later
* License URI: license.txt
* Text Domain: code-snippets
- * Version: 3.9.3
+ * Version: 3.9.4
* Requires PHP: 7.4
* Requires at least: 5.0
*
- * @version 3.9.3
+ * @version 3.9.4
* @package Code_Snippets
* @author Shea Bunge
* @copyright 2012-2024 Code Snippets Pro
@@ -37,7 +37,7 @@
*
* @const string
*/
- define( 'CODE_SNIPPETS_VERSION', '3.9.3' );
+ define( 'CODE_SNIPPETS_VERSION', '3.9.4' );
/**
* The full path to the main file of this plugin.
diff --git a/src/css/admin-bar.scss b/src/css/admin-bar.scss
new file mode 100644
index 000000000..3ace53913
--- /dev/null
+++ b/src/css/admin-bar.scss
@@ -0,0 +1,119 @@
+#wpadminbar {
+ #wp-admin-bar-code-snippets > .ab-item {
+ .code-snippets-admin-bar-icon {
+ width: 16px;
+ height: 16px;
+ top: 5px;
+
+ &::before {
+ content: '';
+ display: block;
+ width: 16px;
+ height: 16px;
+ mask-image: url('../assets/menu-icon.svg');
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ background-color: rgba(240, 245, 250, 0.6);
+ }
+ }
+ }
+
+ .code-snippets-safe-mode-active {
+ > .ab-item,
+ &.ab-item {
+ background: #b32d2e;
+ color: #fff;
+ }
+
+ &:hover > .ab-item,
+ &.hover > .ab-item {
+ background: #d63638;
+ color: #fff;
+ }
+ }
+
+ .code-snippets-safe-mode.code-snippets-safe-mode-active > .ab-item,
+ .code-snippets-safe-mode.code-snippets-safe-mode-active.ab-item {
+ font-weight: 600;
+ }
+
+ .code-snippets-safe-mode .code-snippets-external-icon {
+ font-family: dashicons;
+ display: inline-block;
+ position: relative;
+ line-height: 1;
+ opacity: .8;
+ top: 6px;
+ }
+
+ .code-snippets-safe-mode:hover .code-snippets-external-icon,
+ .code-snippets-safe-mode.hover .code-snippets-external-icon {
+ opacity: 1;
+ }
+
+ #wp-admin-bar-code-snippets-active-snippets > .ab-sub-wrapper > .ab-submenu,
+ #wp-admin-bar-code-snippets-inactive-snippets > .ab-sub-wrapper > .ab-submenu {
+ padding-top: 0;
+ }
+
+ .code-snippets-disabled > .ab-item {
+ opacity: 0.6;
+ }
+
+ .code-snippets-disabled:hover > .ab-item,
+ .code-snippets-disabled.hover > .ab-item {
+ opacity: 1;
+ }
+
+ .code-snippets-pagination-node > .ab-item {
+ padding: 0;
+ margin-bottom: 6px;
+ }
+
+ .code-snippets-pagination-controls {
+ display: flex;
+ align-items: stretch;
+ width: 100%;
+ white-space: nowrap;
+
+ .code-snippets-pagination-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1 1 0;
+ min-height: 32px;
+ padding: 0 10px;
+ background: rgba(240, 245, 250, 0.08);
+ color: inherit;
+ text-decoration: none;
+ line-height: 1.2;
+ box-sizing: border-box;
+
+ & + .code-snippets-pagination-button {
+ border-left: 1px solid rgba(240, 245, 250, 0.15);
+ }
+
+ &[aria-disabled='true'] {
+ opacity: 0.4;
+ pointer-events: none;
+ }
+ }
+
+ a.code-snippets-pagination-button:hover {
+ background: rgba(240, 245, 250, 0.16);
+ }
+
+ a[data-action='first'],
+ a[data-action='last'] {
+ flex: 0 0 36px;
+ padding: 0;
+ }
+
+ .code-snippets-pagination-page {
+ flex: 0 0 auto;
+ cursor: default;
+ opacity: 0.85;
+ }
+ }
+}
diff --git a/src/js/admin-bar.ts b/src/js/admin-bar.ts
new file mode 100644
index 000000000..29e9e3097
--- /dev/null
+++ b/src/js/admin-bar.ts
@@ -0,0 +1,296 @@
+type PaginationStatus = 'active' | 'inactive'
+type PaginationAction = 'first' | 'prev' | 'next' | 'last'
+
+type SnippetResponseItem = {
+ id: number
+ scope: string
+ name?: string
+}
+
+type AdminBarConfig = {
+ restUrl: string
+ nonce: string
+ perPage: number
+ isNetwork: boolean
+ excludeTypes: string[]
+ snippetPlaceholder: string
+ editUrlBase: string
+ activeNodeId: string
+ inactiveNodeId: string
+}
+
+declare const CODE_SNIPPETS_ADMIN_BAR: AdminBarConfig | undefined
+
+const config = typeof CODE_SNIPPETS_ADMIN_BAR !== 'undefined' ? CODE_SNIPPETS_ADMIN_BAR : undefined
+
+const getMenuNode = (status: PaginationStatus): HTMLElement | null => {
+ if (!config) return null
+ const nodeId = status === 'active' ? config.activeNodeId : config.inactiveNodeId
+ return document.getElementById(nodeId)
+}
+
+const getPaginationControls = (status: PaginationStatus): HTMLElement | null => {
+ const menuNode = getMenuNode(status)
+ if (!menuNode) return null
+
+ return menuNode.querySelector(`.code-snippets-pagination-controls[data-status="${status}"]`)
+}
+
+const getPaginationState = (controls: HTMLElement): { page: number; totalPages: number } => {
+ const page = Number.parseInt(controls.dataset.page ?? '1', 10)
+ const totalPages = Number.parseInt(controls.dataset.totalPages ?? '1', 10)
+
+ return {
+ page: Number.isFinite(page) && page > 0 ? page : 1,
+ totalPages: Number.isFinite(totalPages) && totalPages > 0 ? totalPages : 1
+ }
+}
+
+const setLoading = (controls: HTMLElement, loading: boolean) => {
+ controls.dataset.loading = loading ? 'true' : 'false'
+}
+
+const buildRequestUrl = (status: PaginationStatus, page: number): string => {
+ if (!config) return ''
+
+ const url = new URL(config.restUrl)
+ url.searchParams.set('status', status)
+ url.searchParams.set('page', String(page))
+ url.searchParams.set('per_page', String(config.perPage))
+ url.searchParams.set('orderby', 'display_name')
+ url.searchParams.set('order', 'asc')
+
+ if (config.isNetwork) {
+ url.searchParams.set('network', '1')
+ }
+
+ for (const excluded of config.excludeTypes ?? []) {
+ url.searchParams.append('exclude_types[]', excluded)
+ }
+
+ return url.toString()
+}
+
+const fetchSnippetsPage = async (status: PaginationStatus, page: number) => {
+ if (!config) {
+ throw new Error('Missing CODE_SNIPPETS_ADMIN_BAR config')
+ }
+
+ const response = await fetch(buildRequestUrl(status, page), {
+ credentials: 'same-origin',
+ headers: {
+ 'X-WP-Nonce': config.nonce
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch snippets (${response.status})`)
+ }
+
+ const totalPagesHeader = response.headers.get('X-WP-TotalPages') ?? '1'
+ const totalPages = Number.parseInt(totalPagesHeader, 10) || 1
+
+ const snippets = (await response.json()) as SnippetResponseItem[]
+
+ return { snippets, totalPages }
+}
+
+const buildEditUrl = (snippetId: number): string => {
+ if (!config) return '#'
+
+ try {
+ const url = new URL(config.editUrlBase, window.location.href)
+ url.searchParams.set('id', String(snippetId))
+ return url.toString()
+ } catch {
+ return '#'
+ }
+}
+
+const buildSnippetPlaceholder = (snippetId: number): string => {
+ if (!config?.snippetPlaceholder) return `Snippet #${snippetId}`
+
+ return config.snippetPlaceholder.replace(/%(\d+\$)?d/, String(snippetId))
+}
+
+const getTypeFromScope = (scope: string): string => {
+ if (scope.endsWith('-css')) return 'css'
+ if (scope.endsWith('-js')) return 'js'
+ if (scope.endsWith('content')) return 'html'
+ if (scope === 'condition') return 'cond'
+ return 'php'
+}
+
+const formatSnippetTitle = (snippet: SnippetResponseItem): string => {
+ const typeLabel = getTypeFromScope(snippet.scope).toUpperCase()
+ const name = snippet.name?.trim()
+ const title = name ? name : buildSnippetPlaceholder(snippet.id)
+ return `(${typeLabel}) ${title}`
+}
+
+const updatePaginationControls = (controls: HTMLElement, page: number, totalPages: number) => {
+ controls.dataset.page = String(page)
+ controls.dataset.totalPages = String(totalPages)
+
+ const pageLabel = controls.querySelector('.code-snippets-pagination-page')
+ if (pageLabel) {
+ pageLabel.textContent = pageLabel.textContent?.replace(/\(\d+\/\d+\)/, `(${page}/${totalPages})`) ?? pageLabel.textContent
+ }
+
+ const disableFirstPrev = page <= 1
+ const disableNextLast = page >= totalPages
+
+ const setDisabled = (action: PaginationAction, disabled: boolean) => {
+ const link = controls.querySelector(`a[data-action="${action}"]`)
+ if (link) {
+ link.setAttribute('aria-disabled', disabled ? 'true' : 'false')
+ }
+ }
+
+ setDisabled('first', disableFirstPrev)
+ setDisabled('prev', disableFirstPrev)
+ setDisabled('next', disableNextLast)
+ setDisabled('last', disableNextLast)
+
+ const queryArg = controls.dataset.queryArg
+ if (queryArg) {
+ const firstLink = controls.querySelector('a[data-action="first"]')
+ const baseHref = firstLink?.href
+
+ if (baseHref) {
+ const buildHref = (targetPage: number) => {
+ const url = new URL(baseHref)
+
+ if (targetPage <= 1) {
+ url.searchParams.delete(queryArg)
+ } else {
+ url.searchParams.set(queryArg, String(targetPage))
+ }
+
+ return url.toString()
+ }
+
+ const getLink = (action: PaginationAction) =>
+ controls.querySelector(`a[data-action="${action}"]`)
+
+ const first = getLink('first')
+ if (first) first.href = buildHref(1)
+
+ const prev = getLink('prev')
+ if (prev) prev.href = buildHref(Math.max(1, page - 1))
+
+ const next = getLink('next')
+ if (next) next.href = buildHref(Math.min(totalPages, page + 1))
+
+ const last = getLink('last')
+ if (last) last.href = buildHref(totalPages)
+ }
+ }
+}
+
+const replaceSnippetItems = (status: PaginationStatus, snippets: SnippetResponseItem[]) => {
+ const menuNode = getMenuNode(status)
+ if (!menuNode) return
+
+ const subMenu = menuNode.querySelector('ul.ab-submenu')
+ if (!subMenu) return
+
+ subMenu.querySelectorAll('li.code-snippets-snippet-item').forEach(node => node.remove())
+
+ const insertAfterId = status === 'active'
+ ? 'wp-admin-bar-code-snippets-active-pagination'
+ : 'wp-admin-bar-code-snippets-inactive-pagination'
+
+ const insertAfter = subMenu.querySelector(`#${insertAfterId}`)
+ const fragment = document.createDocumentFragment()
+
+ for (const snippet of snippets) {
+ const li = document.createElement('li')
+ li.id = `wp-admin-bar-code-snippets-snippet-${snippet.id}`
+ li.className = 'code-snippets-snippet-item'
+
+ const a = document.createElement('a')
+ a.className = 'ab-item'
+ a.href = buildEditUrl(snippet.id)
+ a.textContent = formatSnippetTitle(snippet)
+
+ li.appendChild(a)
+ fragment.appendChild(li)
+ }
+
+ if (insertAfter && insertAfter.parentNode === subMenu) {
+ subMenu.insertBefore(fragment, insertAfter.nextSibling)
+ } else {
+ subMenu.appendChild(fragment)
+ }
+}
+
+const navigateToPage = async (status: PaginationStatus, targetPage: number) => {
+ const controls = getPaginationControls(status)
+ if (!controls) return
+
+ const { totalPages: currentTotalPages } = getPaginationState(controls)
+ const page = Math.max(1, Math.min(targetPage, currentTotalPages))
+
+ setLoading(controls, true)
+
+ try {
+ const { snippets, totalPages } = await fetchSnippetsPage(status, page)
+ updatePaginationControls(controls, page, totalPages)
+ replaceSnippetItems(status, snippets)
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error)
+ } finally {
+ setLoading(controls, false)
+ }
+}
+
+const handlePaginationClick = (event: MouseEvent) => {
+ const target = event.target as Element | null
+ if (!target) return
+
+ const link = target.closest('.code-snippets-pagination-controls a[data-action]')
+ if (!link) return
+
+ const controls = link.closest('.code-snippets-pagination-controls')
+ if (!controls) return
+ if (controls.dataset.loading === 'true') return
+
+ const status = controls.dataset.status as PaginationStatus | undefined
+ const action = link.dataset.action as PaginationAction | undefined
+
+ if (!status || !action) return
+
+ event.preventDefault()
+ event.stopPropagation()
+ event.stopImmediatePropagation()
+
+ const { page, totalPages } = getPaginationState(controls)
+
+ let targetPage = page
+ switch (action) {
+ case 'first':
+ targetPage = 1
+ break
+ case 'prev':
+ targetPage = page - 1
+ break
+ case 'next':
+ targetPage = page + 1
+ break
+ case 'last':
+ targetPage = totalPages
+ break
+ }
+
+ if (targetPage === page || targetPage < 1 || targetPage > totalPages) {
+ return
+ }
+
+ void navigateToPage(status, targetPage)
+}
+
+if (config) {
+ document.addEventListener('click', handlePaginationClick, true)
+}
diff --git a/src/php/admin-menus/class-welcome-menu.php b/src/php/admin-menus/class-welcome-menu.php
index ceb816ac3..02788d4e1 100644
--- a/src/php/admin-menus/class-welcome-menu.php
+++ b/src/php/admin-menus/class-welcome-menu.php
@@ -59,7 +59,7 @@ protected function get_header_links(): array {
'label' => __( 'Cloud', 'code-snippets' ),
],
'resources' => [
- 'url' => 'https://help.codesnippets.pro/',
+ 'url' => 'https://codesnippets.pro/support/',
'icon' => 'sos',
'label' => __( 'Support', 'code-snippets' ),
],
diff --git a/src/php/class-admin-bar.php b/src/php/class-admin-bar.php
new file mode 100644
index 000000000..706a75e6d
--- /dev/null
+++ b/src/php/class-admin-bar.php
@@ -0,0 +1,546 @@
+ esc_url_raw( rest_url( Snippets_REST_Controller::get_base_route() ) ),
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ 'perPage' => $this->get_snippet_limit(),
+ 'isNetwork' => is_network_admin(),
+ 'excludeTypes' => [ 'cond' ],
+ 'snippetPlaceholder' => esc_html__( 'Snippet #%d', 'code-snippets' ),
+ 'editUrlBase' => code_snippets()->get_menu_url( 'edit' ),
+ 'activeNodeId' => 'wp-admin-bar-' . self::ROOT_NODE_ID . '-active-snippets',
+ 'inactiveNodeId' => 'wp-admin-bar-' . self::ROOT_NODE_ID . '-inactive-snippets',
+ ]
+ );
+ }
+
+ /**
+ * Register admin bar nodes.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
+ *
+ * @return void
+ */
+ public function register_nodes( WP_Admin_Bar $wp_admin_bar ): void {
+ if ( ! is_admin_bar_showing() ) {
+ return;
+ }
+
+ if ( ! code_snippets()->current_user_can() ) {
+ return;
+ }
+
+ $is_safe_mode_active = code_snippets()->evaluate_functions->is_safe_mode_active();
+
+ // Always show safe mode indicator regardless of setting.
+ $this->add_safe_mode_nodes( $wp_admin_bar, $is_safe_mode_active );
+
+ // Check if admin bar menu is enabled via settings.
+ $is_enabled = Settings\get_setting( 'general', 'enable_admin_bar' );
+
+ if ( ! $is_enabled && ! apply_filters( 'code_snippets/admin_bar/enabled', false ) ) {
+ return;
+ }
+
+ $title = sprintf(
+ '%s',
+ esc_html__( 'Snippets', 'code-snippets' )
+ );
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID,
+ 'title' => $title,
+ 'href' => code_snippets()->get_menu_url( 'manage' ),
+ ]
+ );
+
+ $this->add_quick_links( $wp_admin_bar );
+ $this->add_snippet_listings( $wp_admin_bar );
+ $this->add_safe_mode_link( $wp_admin_bar );
+ }
+
+ /**
+ * Add menu item for safe mode status.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
+ * @param bool $is_safe_mode_active Whether safe mode is active.
+ *
+ * @return void
+ */
+ private function add_safe_mode_nodes( WP_Admin_Bar $wp_admin_bar, bool $is_safe_mode_active ): void {
+ if ( ! $is_safe_mode_active ) {
+ return;
+ }
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::SAFE_MODE_NODE_ID,
+ 'title' => esc_html__( 'Snippets Safe Mode Active', 'code-snippets' ),
+ 'href' => 'https://snipco.de/safe-mode',
+ 'parent' => 'top-secondary',
+ 'meta' => [
+ 'class' => 'code-snippets-safe-mode code-snippets-safe-mode-active',
+ 'target' => '_blank',
+ 'rel' => 'noopener noreferrer',
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Add a safe mode documentation link under the Code Snippets menu.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
+ *
+ * @return void
+ */
+ private function add_safe_mode_link( WP_Admin_Bar $wp_admin_bar ): void {
+ $title = sprintf(
+ '%s ',
+ esc_html__( 'Safe Mode', 'code-snippets' )
+ );
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-safe-mode-doc',
+ 'title' => $title,
+ 'href' => 'https://snipco.de/safe-mode',
+ 'parent' => self::ROOT_NODE_ID,
+ 'meta' => [
+ 'class' => 'code-snippets-safe-mode',
+ 'target' => '_blank',
+ 'rel' => 'noopener noreferrer',
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Add quick links to common Code Snippets screens.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
+ *
+ * @return void
+ */
+ private function add_quick_links( WP_Admin_Bar $wp_admin_bar ): void {
+ $plugin = code_snippets();
+ $is_licensed = $plugin->licensing->is_licensed();
+ $upgrade_url = self_admin_url( 'admin.php?page=code_snippets_upgrade' );
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-manage',
+ 'title' => esc_html_x( 'Manage', 'snippets', 'code-snippets' ),
+ 'href' => $plugin->get_menu_url( 'manage' ),
+ 'parent' => self::ROOT_NODE_ID,
+ ]
+ );
+
+ $statuses = [
+ 'all' => _x( 'All Snippets', 'snippets', 'code-snippets' ),
+ 'active' => _x( 'Active Snippets', 'snippets', 'code-snippets' ),
+ 'inactive' => _x( 'Inactive Snippets', 'snippets', 'code-snippets' ),
+ ];
+
+ foreach ( $statuses as $status => $label ) {
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . "-status-$status",
+ 'title' => esc_html( $label ),
+ 'href' => esc_url( add_query_arg( 'status', $status, $plugin->get_menu_url( 'manage' ) ) ),
+ 'parent' => self::ROOT_NODE_ID . '-manage',
+ ]
+ );
+ }
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-add',
+ 'title' => esc_html_x( 'Add New', 'snippet', 'code-snippets' ),
+ 'href' => $plugin->get_menu_url( 'add' ),
+ 'parent' => self::ROOT_NODE_ID,
+ ]
+ );
+
+ $types = [
+ 'php' => _x( 'Functions (PHP)', 'snippet type', 'code-snippets' ),
+ 'html' => _x( 'Content (HTML)', 'snippet type', 'code-snippets' ),
+ 'css' => _x( 'Styles (CSS)', 'snippet type', 'code-snippets' ),
+ 'js' => _x( 'Scripts (JS)', 'snippet type', 'code-snippets' ),
+ 'cond' => _x( 'Conditions (COND)', 'snippet type', 'code-snippets' ),
+ ];
+
+ $types = array_intersect_key( $types, array_flip( Snippet::get_types() ) );
+ $pro_types = [ 'css', 'js', 'cond' ];
+
+ foreach ( $types as $type => $label ) {
+ $is_disabled = in_array( $type, $pro_types, true ) && ! $is_licensed;
+
+ $url = $is_disabled ?
+ $upgrade_url :
+ add_query_arg( 'type', $type, $plugin->get_menu_url( 'add' ) );
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . "-add-$type",
+ 'title' => esc_html( $label ),
+ 'href' => esc_url( $url ),
+ 'parent' => self::ROOT_NODE_ID . '-add',
+ 'meta' => $is_disabled ? [ 'class' => 'code-snippets-disabled' ] : [],
+ ]
+ );
+ }
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-import',
+ 'title' => esc_html_x( 'Import', 'snippets', 'code-snippets' ),
+ 'href' => $plugin->get_menu_url( 'import' ),
+ 'parent' => self::ROOT_NODE_ID,
+ ]
+ );
+
+ $settings_context = Settings\are_settings_unified() ? 'network' : 'admin';
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-settings',
+ 'title' => esc_html_x( 'Settings', 'snippets', 'code-snippets' ),
+ 'href' => $plugin->get_menu_url( 'settings', $settings_context ),
+ 'parent' => self::ROOT_NODE_ID,
+ ]
+ );
+ }
+
+ /**
+ * Add a list of snippets under the active and inactive statuses.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
+ *
+ * @return void
+ */
+ private function add_snippet_listings( WP_Admin_Bar $wp_admin_bar ): void {
+ $items_per_page = $this->get_snippet_limit();
+ if ( $items_per_page < 1 ) {
+ return;
+ }
+
+ $plugin = code_snippets();
+
+ $snippets = array_filter(
+ get_snippets(),
+ static function ( Snippet $snippet ): bool {
+ return ! $snippet->is_trashed() && ! $snippet->is_condition();
+ }
+ );
+
+ $active_snippets = array_values(
+ array_filter(
+ $snippets,
+ static function ( Snippet $snippet ): bool {
+ return $snippet->active;
+ }
+ )
+ );
+
+ $inactive_snippets = array_values(
+ array_filter(
+ $snippets,
+ static function ( Snippet $snippet ): bool {
+ return ! $snippet->active;
+ }
+ )
+ );
+
+ usort(
+ $active_snippets,
+ static function ( Snippet $a, Snippet $b ): int {
+ return strcasecmp( $a->display_name, $b->display_name );
+ }
+ );
+
+ usort(
+ $inactive_snippets,
+ static function ( Snippet $a, Snippet $b ): int {
+ return strcasecmp( $a->display_name, $b->display_name );
+ }
+ );
+
+ $active_page = isset( $_GET[ self::ACTIVE_PAGE_QUERY_ARG ] ) ? absint( $_GET[ self::ACTIVE_PAGE_QUERY_ARG ] ) : 1;
+ $inactive_page = isset( $_GET[ self::INACTIVE_PAGE_QUERY_ARG ] ) ? absint( $_GET[ self::INACTIVE_PAGE_QUERY_ARG ] ) : 1;
+
+ $active_total_pages = max( 1, (int) ceil( count( $active_snippets ) / $items_per_page ) );
+ $inactive_total_pages = max( 1, (int) ceil( count( $inactive_snippets ) / $items_per_page ) );
+
+ $active_page = max( 1, min( $active_page, $active_total_pages ) );
+ $inactive_page = max( 1, min( $inactive_page, $inactive_total_pages ) );
+
+ $active_offset = ( $active_page - 1 ) * $items_per_page;
+ $inactive_offset = ( $inactive_page - 1 ) * $items_per_page;
+
+ $active_page_snippets = array_slice( $active_snippets, $active_offset, $items_per_page );
+ $inactive_page_snippets = array_slice( $inactive_snippets, $inactive_offset, $items_per_page );
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-active-snippets',
+ 'title' => sprintf(
+ /* translators: %d: number of active snippets. */
+ esc_html__( 'Active Snippets (%d)', 'code-snippets' ),
+ count( $active_snippets )
+ ),
+ 'href' => esc_url( add_query_arg( 'status', 'active', $plugin->get_menu_url( 'manage' ) ) ),
+ 'parent' => self::ROOT_NODE_ID,
+ ]
+ );
+
+ if ( $active_total_pages > 1 ) {
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-active-pagination',
+ 'title' => $this->get_pagination_controls_html( 'active', $active_page, $active_total_pages, self::ACTIVE_PAGE_QUERY_ARG ),
+ 'parent' => self::ROOT_NODE_ID . '-active-snippets',
+ 'meta' => [ 'class' => 'code-snippets-pagination-node' ],
+ ]
+ );
+ }
+
+ foreach ( $active_page_snippets as $snippet ) {
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-snippet-' . $snippet->id,
+ 'title' => esc_html( $this->format_snippet_title( $snippet ) ),
+ 'href' => esc_url( $plugin->get_snippet_edit_url( $snippet->id ) ),
+ 'parent' => self::ROOT_NODE_ID . '-active-snippets',
+ 'meta' => [ 'class' => 'code-snippets-snippet-item' ],
+ ]
+ );
+ }
+
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-inactive-snippets',
+ 'title' => sprintf(
+ /* translators: %d: number of inactive snippets. */
+ esc_html__( 'Inactive Snippets (%d)', 'code-snippets' ),
+ count( $inactive_snippets )
+ ),
+ 'href' => esc_url( add_query_arg( 'status', 'inactive', $plugin->get_menu_url( 'manage' ) ) ),
+ 'parent' => self::ROOT_NODE_ID,
+ ]
+ );
+
+ if ( $inactive_total_pages > 1 ) {
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-inactive-pagination',
+ 'title' => $this->get_pagination_controls_html( 'inactive', $inactive_page, $inactive_total_pages, self::INACTIVE_PAGE_QUERY_ARG ),
+ 'parent' => self::ROOT_NODE_ID . '-inactive-snippets',
+ 'meta' => [ 'class' => 'code-snippets-pagination-node' ],
+ ]
+ );
+ }
+
+ foreach ( $inactive_page_snippets as $snippet ) {
+ $wp_admin_bar->add_node(
+ [
+ 'id' => self::ROOT_NODE_ID . '-snippet-' . $snippet->id,
+ 'title' => esc_html( $this->format_snippet_title( $snippet ) ),
+ 'href' => esc_url( $plugin->get_snippet_edit_url( $snippet->id ) ),
+ 'parent' => self::ROOT_NODE_ID . '-inactive-snippets',
+ 'meta' => [ 'class' => 'code-snippets-snippet-item' ],
+ ]
+ );
+ }
+ }
+
+ /**
+ * Build an admin bar snippet title including type prefix.
+ *
+ * @param Snippet $snippet Snippet object.
+ *
+ * @return string
+ */
+ private function format_snippet_title( Snippet $snippet ): string {
+ return sprintf( '(%s) %s', strtoupper( $snippet->type ), $snippet->display_name );
+ }
+
+ /**
+ * Retrieve the number of snippets to show per page in the admin bar.
+ *
+ * @return int
+ */
+ private function get_snippet_limit(): int {
+ $limit = (int) Settings\get_setting( 'general', 'admin_bar_snippet_limit' );
+
+ if ( $limit < 1 ) {
+ $limit = 20;
+ }
+
+ return max( 1, (int) apply_filters( 'code_snippets/admin_bar/snippet_limit', $limit ) );
+ }
+
+ /**
+ * Build pagination controls HTML for a snippet listing submenu.
+ *
+ * @param string $status Snippet status: "active" or "inactive".
+ * @param int $page Current page.
+ * @param int $total_pages Total pages.
+ * @param string $page_query_arg Query arg used for progressive enhancement.
+ *
+ * @return string
+ */
+ private function get_pagination_controls_html( string $status, int $page, int $total_pages, string $page_query_arg ): string {
+ $first_url = remove_query_arg( $page_query_arg );
+ $prev_url = $page > 2 ? add_query_arg( $page_query_arg, $page - 1 ) : remove_query_arg( $page_query_arg );
+ $next_url = add_query_arg( $page_query_arg, $page + 1 );
+ $last_url = add_query_arg( $page_query_arg, $total_pages );
+
+ $disabled_first = $page <= 1 ? 'true' : 'false';
+ $disabled_prev = $page <= 1 ? 'true' : 'false';
+ $disabled_next = $page >= $total_pages ? 'true' : 'false';
+ $disabled_last = $page >= $total_pages ? 'true' : 'false';
+
+ $html = sprintf(
+ '',
+ esc_attr( $status ),
+ $page,
+ $total_pages,
+ esc_url( $first_url ),
+ esc_url( $prev_url ),
+ esc_html__( 'Back', 'code-snippets' ),
+ esc_html__( 'Page', 'code-snippets' ),
+ esc_url( $next_url ),
+ $disabled_first,
+ $disabled_prev,
+ $disabled_next,
+ esc_html__( 'Next', 'code-snippets' ),
+ esc_url( $last_url ),
+ $disabled_last,
+ esc_attr( $page_query_arg )
+ );
+
+ return wp_kses(
+ $html,
+ [
+ 'span' => [
+ 'class' => [],
+ 'data-status' => [],
+ 'data-page' => [],
+ 'data-total-pages' => [],
+ 'data-query-arg' => [],
+ ],
+ 'a' => [
+ 'class' => [],
+ 'href' => [],
+ 'data-action' => [],
+ 'aria-disabled' => [],
+ ],
+ ]
+ );
+ }
+}
diff --git a/src/php/class-admin.php b/src/php/class-admin.php
index dfdf093dd..9b109b5df 100644
--- a/src/php/class-admin.php
+++ b/src/php/class-admin.php
@@ -179,7 +179,7 @@ public function plugin_row_meta( array $plugin_meta, string $plugin_file ): arra
array(
sprintf(
$format,
- 'https://help.codesnippets.pro/',
+ 'https://codesnippets.pro/support/',
esc_attr__( 'Find out how to get support with Code Snippets', 'code-snippets' ),
esc_html__( 'Docs and Support', 'code-snippets' )
),
diff --git a/src/php/class-contextual-help.php b/src/php/class-contextual-help.php
index 267d55ff3..85b69e533 100644
--- a/src/php/class-contextual-help.php
+++ b/src/php/class-contextual-help.php
@@ -67,15 +67,15 @@ public function load() {
private function load_help_sidebar() {
$sidebar_links = [
'https://wordpress.org/plugins/code-snippets' => __( 'About Plugin', 'code-snippets' ),
- 'https://help.codesnippets.pro/collection/3-faq' => __( 'FAQ', 'code-snippets' ),
+ 'https://codesnippets.pro/docs/faq/' => __( 'FAQ', 'code-snippets' ),
'https://wordpress.org/support/plugin/code-snippets' => __( 'Support Forum', 'code-snippets' ),
'https://codesnippets.pro' => __( 'Plugin Website', 'code-snippets' ),
];
$kses = [
- 'p' => [],
+ 'p' => [],
'strong' => [],
- 'a' => [ 'href' => [] ],
+ 'a' => [ 'href' => [] ],
];
$contents = sprintf( "%s
\n", esc_html__( 'For more information:', 'code-snippets' ) );
@@ -143,7 +143,7 @@ private function load_manage_help() {
__( 'Be sure to check your snippets for errors before you activate them, as a faulty snippet could bring your whole blog down. If your site starts doing strange things, deactivate all your snippets and activate them one at a time.', 'code-snippets' ),
__( "If something goes wrong with a snippet, and you can't use WordPress, you can cause all snippets to stop executing by turning on safe mode.", 'code-snippets' ),
/* translators: %s: URL to Code Snippets Pro Docs */
- sprintf( __( 'You can find out how to enable safe mode in the Code Snippets Pro Docs.', 'code-snippets' ), 'https://help.codesnippets.pro/article/12-safe-mode' )
+ sprintf( __( 'You can find out how to enable safe mode in the Code Snippets Pro Docs.', 'code-snippets' ), 'https://codesnippets.pro/doc/safe-mode/' ),
]
);
}
@@ -159,7 +159,7 @@ private function load_edit_help() {
$this->get_intro_text() .
__( 'Here you can add a new snippet, or edit an existing one.', 'code-snippets' ),
/* translators: %s: URL to Code Snippets Pro Docs */
- sprintf( __( "If you're not sure about the types of snippets you can add, take a look at the Code Snippets Pro Docs for inspiration.", 'code-snippets' ), 'https://help.codesnippets.pro/collection/2-adding-snippets' ),
+ sprintf( __( "If you're not sure about the types of snippets you can add, take a look at the Code Snippets Pro Docs for inspiration.", 'code-snippets' ), 'https://codesnippets.pro/docs/adding-snippets/' ),
]
);
diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php
index 7f5390c63..eb8f9d96c 100644
--- a/src/php/class-plugin.php
+++ b/src/php/class-plugin.php
@@ -84,6 +84,13 @@ class Plugin {
*/
public Snippet_Handler_Registry $snippet_handler_registry;
+ /**
+ * Admin bar integration class.
+ *
+ * @var Admin_Bar
+ */
+ public Admin_Bar $admin_bar;
+
/**
* Class constructor
*
@@ -121,6 +128,11 @@ public function load_plugin() {
$this->evaluate_content = new Evaluate_Content( $this->db );
$this->evaluate_functions = new Evaluate_Functions( $this->db );
+ // Admin bar integration.
+ require_once $includes_path . '/class-admin-bar.php';
+ $this->admin_bar = new Admin_Bar();
+ $this->admin_bar->register_hooks();
+
// CodeMirror editor functions.
require_once $includes_path . '/editor.php';
@@ -394,6 +406,7 @@ public static function get_types(): array {
'html' => __( 'Content', 'code-snippets' ),
'css' => __( 'Styles', 'code-snippets' ),
'js' => __( 'Scripts', 'code-snippets' ),
+ 'cond' => __( 'Conditions', 'code-snippets' ),
'cloud' => __( 'Codevault', 'code-snippets' ),
'cloud_search' => __( 'Cloud Search', 'code-snippets' ),
'bundles' => __( 'Bundles', 'code-snippets' ),
diff --git a/src/php/migration/importers/files/file-upload-importer.php b/src/php/migration/importers/files/file-upload-importer.php
index e4ec32f91..84d82005e 100644
--- a/src/php/migration/importers/files/file-upload-importer.php
+++ b/src/php/migration/importers/files/file-upload-importer.php
@@ -280,7 +280,7 @@ private function parse_json_file( string $file_path, string $file_name ) {
'id' => $snippet_data['id'] ?? uniqid(),
'title' => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
'scope' => $snippet_data['scope'] ?? 'global',
- 'tags' => is_array( $snippet_data['tags'] ?? [] ) ? implode( ', ', $snippet_data['tags'] ) : '',
+ 'tags' => is_array( $snippet_data['tags'] ?? null ) ? implode( ', ', $snippet_data['tags'] ) : '',
'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
'type' => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' )
];
diff --git a/src/php/rest-api/class-snippets-rest-controller.php b/src/php/rest-api/class-snippets-rest-controller.php
index bf37e60bf..f8478cb82 100644
--- a/src/php/rest-api/class-snippets-rest-controller.php
+++ b/src/php/rest-api/class-snippets-rest-controller.php
@@ -83,6 +83,42 @@ public function register_routes() {
// Allow standard collection parameters (page, per_page, etc.) on the collection route.
$collection_args = array_merge( $network_args, $this->get_collection_params() );
+ $collection_args['status'] = [
+ 'description' => esc_html__( 'Filter snippets by activation status.', 'code-snippets' ),
+ 'type' => 'string',
+ 'enum' => [ 'all', 'active', 'inactive' ],
+ 'default' => 'all',
+ 'sanitize_callback' => 'sanitize_key',
+ ];
+
+ $collection_args['exclude_types'] = [
+ 'description' => esc_html__( 'List of snippet types to exclude from the response.', 'code-snippets' ),
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'string',
+ ],
+ 'default' => [],
+ 'sanitize_callback' => static function ( $value ): array {
+ $values = is_array( $value ) ? $value : [ $value ];
+ return array_values( array_filter( array_map( 'sanitize_key', $values ) ) );
+ },
+ ];
+
+ $collection_args['orderby'] = [
+ 'description' => esc_html__( 'Sort collection by object attribute.', 'code-snippets' ),
+ 'type' => 'string',
+ 'enum' => [ 'id', 'name', 'display_name' ],
+ 'sanitize_callback' => 'sanitize_key',
+ ];
+
+ $collection_args['order'] = [
+ 'description' => esc_html__( 'Sort direction.', 'code-snippets' ),
+ 'type' => 'string',
+ 'enum' => [ 'asc', 'desc' ],
+ 'default' => 'asc',
+ 'sanitize_callback' => 'sanitize_key',
+ ];
+
register_rest_route(
$this->namespace,
$route,
@@ -200,6 +236,72 @@ public function get_items( $request ): WP_REST_Response {
$all_snippets = get_snippets( [], $network );
$all_snippets = $this->get_network_items( $all_snippets, $network );
+ $status = sanitize_key( (string) $request->get_param( 'status' ) );
+
+ if ( in_array( $status, [ 'active', 'inactive' ], true ) ) {
+ $all_snippets = array_filter(
+ $all_snippets,
+ static function ( Snippet $snippet ): bool {
+ return ! $snippet->is_trashed();
+ }
+ );
+ }
+
+ $exclude_types = $request->get_param( 'exclude_types' );
+ $exclude_types = is_array( $exclude_types ) ? array_map( 'sanitize_key', $exclude_types ) : [];
+
+ if ( $exclude_types ) {
+ $all_snippets = array_filter(
+ $all_snippets,
+ static function ( Snippet $snippet ) use ( $exclude_types ): bool {
+ return ! in_array( $snippet->type, $exclude_types, true );
+ }
+ );
+ }
+
+ if ( 'active' === $status ) {
+ $all_snippets = array_filter(
+ $all_snippets,
+ static function ( Snippet $snippet ): bool {
+ return $snippet->active;
+ }
+ );
+ } elseif ( 'inactive' === $status ) {
+ $all_snippets = array_filter(
+ $all_snippets,
+ static function ( Snippet $snippet ): bool {
+ return ! $snippet->active;
+ }
+ );
+ }
+
+ $orderby = sanitize_key( (string) $request->get_param( 'orderby' ) );
+ $order = sanitize_key( (string) $request->get_param( 'order' ) );
+
+ if ( $orderby ) {
+ $direction = 'desc' === $order ? -1 : 1;
+
+ usort(
+ $all_snippets,
+ static function ( Snippet $a, Snippet $b ) use ( $orderby, $direction ): int {
+ switch ( $orderby ) {
+ case 'display_name':
+ $cmp = strcasecmp( $a->display_name, $b->display_name );
+ break;
+ case 'name':
+ $cmp = strcasecmp( (string) $a->name, (string) $b->name );
+ break;
+ case 'id':
+ default:
+ $cmp = $a->id <=> $b->id;
+ break;
+ }
+
+ return 0 === $cmp ? ( $a->id <=> $b->id ) * $direction : $cmp * $direction;
+ }
+ );
+ }
+
$total_items = count( $all_snippets );
$query_params = $request->get_query_params();
diff --git a/src/php/settings/settings-fields.php b/src/php/settings/settings-fields.php
index 72262cc77..06033d14d 100644
--- a/src/php/settings/settings-fields.php
+++ b/src/php/settings/settings-fields.php
@@ -32,7 +32,9 @@ function get_default_settings(): array {
'disable_prism' => false,
'hide_upgrade_menu' => false,
'complete_uninstall' => false,
- 'enable_flat_files' => false,
+ 'enable_flat_files' => false,
+ 'enable_admin_bar' => true,
+ 'admin_bar_snippet_limit' => 20,
],
'editor' => [
'indent_with_tabs' => true,
@@ -259,5 +261,22 @@ function get_settings_fields(): array {
$fields = apply_filters( 'code_snippets_settings_fields', $fields );
+ $fields['general']['enable_admin_bar'] = [
+ 'name' => __( 'Enable Admin Bar Menu', 'code-snippets' ),
+ 'type' => 'checkbox',
+ 'label' => __( 'Show a Snippets menu in the admin bar for quick access to snippets.', 'code-snippets' ),
+ ];
+
+ if ( get_setting( 'general', 'enable_admin_bar' ) ) {
+ $fields['general']['admin_bar_snippet_limit'] = [
+ 'name' => __( 'Admin Bar Snippets Per Page', 'code-snippets' ),
+ 'type' => 'number',
+ 'desc' => __( 'Number of snippets to show in the admin bar Active/Inactive menus before paginating.', 'code-snippets' ),
+ 'label' => __( 'snippets', 'code-snippets' ),
+ 'min' => 1,
+ 'max' => 100,
+ ];
+ }
+
return $fields;
}
diff --git a/src/php/views/partials/list-table-notices.php b/src/php/views/partials/list-table-notices.php
index c3d345611..ff6b16eab 100644
--- a/src/php/views/partials/list-table-notices.php
+++ b/src/php/views/partials/list-table-notices.php
@@ -30,7 +30,7 @@
printf( esc_html( $text ), 'CODE_SNIPPETS_SAFE_MODE', 'wp-config.php' );
?>
-
+
@@ -45,54 +45,54 @@
$result = sanitize_key( $_REQUEST['result'] );
$result_messages = [
- 'executed' => __( 'Snippet executed.', 'code-snippets' ),
- 'activated' => __( 'Snippet activated.', 'code-snippets' ),
- 'activated-multi' => __( 'Selected snippets activated.', 'code-snippets' ),
- 'deactivated' => __( 'Snippet deactivated.', 'code-snippets' ),
- 'deactivated-multi' => __( 'Selected snippets deactivated.', 'code-snippets' ),
- 'deleted' => __( 'Snippet trashed.', 'code-snippets' ),
- 'deleted-multi' => __( 'Selected snippets trashed.', 'code-snippets' ),
- 'deleted_permanently' => __( 'Snippet permanently deleted.', 'code-snippets' ),
+ 'executed' => __( 'Snippet executed.', 'code-snippets' ),
+ 'activated' => __( 'Snippet activated.', 'code-snippets' ),
+ 'activated-multi' => __( 'Selected snippets activated.', 'code-snippets' ),
+ 'deactivated' => __( 'Snippet deactivated.', 'code-snippets' ),
+ 'deactivated-multi' => __( 'Selected snippets deactivated.', 'code-snippets' ),
+ 'deleted' => __( 'Snippet trashed.', 'code-snippets' ),
+ 'deleted-multi' => __( 'Selected snippets trashed.', 'code-snippets' ),
+ 'deleted_permanently' => __( 'Snippet permanently deleted.', 'code-snippets' ),
'deleted-permanently-multi' => __( 'Selected snippets permanently deleted.', 'code-snippets' ),
- 'restored' => __( 'Snippet restored.', 'code-snippets' ),
- 'restored-multi' => __( 'Selected snippets restored.', 'code-snippets' ),
- 'cloned' => __( 'Snippet cloned.', 'code-snippets' ),
- 'cloned-multi' => __( 'Selected snippets cloned.', 'code-snippets' ),
- 'cloud-refreshed' => __( 'Synced cloud data has been successfully refreshed.', 'code-snippets' ),
+ 'restored' => __( 'Snippet restored.', 'code-snippets' ),
+ 'restored-multi' => __( 'Selected snippets restored.', 'code-snippets' ),
+ 'cloned' => __( 'Snippet cloned.', 'code-snippets' ),
+ 'cloned-multi' => __( 'Selected snippets cloned.', 'code-snippets' ),
+ 'cloud-refreshed' => __( 'Synced cloud data has been successfully refreshed.', 'code-snippets' ),
];
// Add undo link for single snippet trash action
if ( 'deleted' === $result && ! empty( $_REQUEST['ids'] ) ) {
$deleted_ids = sanitize_text_field( $_REQUEST['ids'] );
- $undo_url = wp_nonce_url(
- add_query_arg( array(
- 'action' => 'restore',
- 'ids' => $deleted_ids
- ) ),
+ $undo_url = wp_nonce_url(
+ add_query_arg(
+ [
+ 'action' => 'restore',
+ 'ids' => $deleted_ids,
+ ]
+ ),
'bulk-snippets'
);
- $result_messages['deleted'] = sprintf(
- __( 'Snippet trashed. Undo', 'code-snippets' ),
- esc_url( $undo_url )
- );
+ // translators: %s: Undo URL.
+ $undo_message = __( 'Snippet trashed. Undo', 'code-snippets' );
+ $result_messages['deleted'] = sprintf( $undo_message, esc_url( $undo_url ) );
}
// Add undo link for bulk snippet trash action
if ( 'deleted-multi' === $result && ! empty( $_REQUEST['ids'] ) ) {
$deleted_ids = sanitize_text_field( $_REQUEST['ids'] );
- $undo_url = wp_nonce_url(
- add_query_arg( array(
- 'action' => 'restore',
- 'ids' => $deleted_ids
- ) ),
+ $undo_url = wp_nonce_url(
+ add_query_arg( array(
+ 'action' => 'restore',
+ 'ids' => $deleted_ids,
+ ) ),
'bulk-snippets'
);
- $result_messages['deleted-multi'] = sprintf(
- __( 'Selected snippets trashed. Undo', 'code-snippets' ),
- esc_url( $undo_url )
- );
+ // translators: %s: Undo URL.
+ $undo_message = __( 'Selected snippets trashed. Undo', 'code-snippets' );
+ $result_messages['deleted-multi'] = sprintf( $undo_message, esc_url( $undo_url ) );
}
$result_messages = apply_filters( 'code_snippets/manage/result_messages', $result_messages );
@@ -100,7 +100,7 @@
if ( isset( $result_messages[ $result ] ) ) {
$result_kses = [
'strong' => [],
- 'a' => [
+ 'a' => [
'href' => [],
],
];
diff --git a/src/readme.txt b/src/readme.txt
index 344ab2c04..16514bc46 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -4,7 +4,7 @@ Donate link: https://codesnippets.pro
Tags: code, snippets, multisite, php, css
License: GPL-2.0-or-later
License URI: license.txt
-Stable tag: 3.9.3
+Stable tag: 3.9.4
Tested up to: 6.8
An easy, clean and simple way to enhance your site with code snippets.
@@ -63,10 +63,10 @@ Network Activating Code Snippets through the Network Dashboard will enable a spe
== Frequently Asked Questions ==
-A full list of our Frequently Asked Questions can be found at [help.codesnippets.pro](https://help.codesnippets.pro/collection/3-faq).
+A full list of our Frequently Asked Questions can be found at [codesnippets.pro](https://codesnippets.pro/docs/faq/).
= How can I recover my site if it is crashed by a buggy snippet? =
-You can recover your site by enabling the Code Snippets safe mode feature. Instructions for how to turn it on are available here: .
+You can recover your site by enabling the Code Snippets safe mode feature. Instructions for how to turn it on are available here: .
= Will I lose my snippets if I change the theme or upgrade WordPress? =
No, the snippets are stored in the WordPress database, independent of the theme and unaffected by WordPress upgrades.
@@ -104,6 +104,23 @@ You can report security bugs found in the source code of this plugin through the
== Changelog ==
+= 3.9.4 (2026-01-14) =
+
+__Added__
+
+* New import functionality to migrate snippets from file uploads with drag-and-drop interface
+* Support for importing snippets from other popular plugins (Header Footer Code Manager, Insert Headers and Footers, Insert PHP Code Snippet)
+* Enhanced file based execution support with improved multisite mode compatibility
+
+__Changed__
+
+* Updated links to more recent documentation pages
+
+__Fixed__
+
+* Fixed multisite capability checks in Plugin class
+* Fixed snippet execution logic for multisite support by centralizing trashed snippet handling
+* Fixed multisite snippet handling to ensure local snippets use correct table and filter out trashed snippets
= 3.9.3 (2025-12-03) =