Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ef9afa8
chore(release): bump version to v3.10.2
code-snippets-bot Aug 31, 2026
9fdad70
fix: restore the Run Once action for single-use snippets (#487)
TallblokeUK Aug 31, 2026
3a3569b
fix: scope cached data to the plugin version so downgrades stop fatal…
TallblokeUK Aug 31, 2026
a26b66e
fix: stop aliased field names warning when reading modified fields (#…
TallblokeUK Aug 31, 2026
1ad7bb3
fix: keep saving possible after the session expires (#490)
TallblokeUK Aug 31, 2026
6f1b898
fix: only validate PHP when activating snippets in bulk (#491)
TallblokeUK Aug 31, 2026
61bb492
fix: safe mode fatals with an undefined wp_get_current_user() (#484)
TallblokeUK Aug 31, 2026
4733a0f
docs: rewrite the 3.10.1 changelog and set tested-up-to to 7.1
imantsk Aug 31, 2026
2f0981b
fix: show the run-once confirmation and harden the handler
imantsk Aug 31, 2026
c04f347
fix: unblock changelog generation for release PRs
imantsk Aug 31, 2026
0e0d11b
docs: add the 3.10.2 changelog
imantsk Aug 31, 2026
bfd57dc
docs: update contributors for the 3.10.2 release
imantsk Aug 31, 2026
5693475
chore(release): regenerate changelog for v3.10.2
code-snippets-bot Aug 31, 2026
73c821a
chore(release): regenerate changelog for v3.10.2
code-snippets-bot Aug 31, 2026
2b2ade1
fix: strip unprefixed vendor namespaces from the autoloader (#505)
TallblokeUK Sep 1, 2026
472cbc4
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
8bca49c
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
c9d953a
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
b977c7e
fix: correct the nonce argument in the version switch AJAX handlers (…
TallblokeUK Sep 1, 2026
419a2fc
fix: apply the row truncation Screen Option to snippet names (3.10.2)…
TallblokeUK Sep 1, 2026
ad76f5f
chore(release): regenerate changelog for v3.10.2
code-snippets-bot Sep 1, 2026
b70f723
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
3dfa23f
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
f2970e6
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
5e9f291
Merge branch 'core' into release/v3.10.2/core
imantsk Sep 1, 2026
5ea86b2
docs: normalise 3.10.2 changelog and readme formatting
imantsk Sep 1, 2026
af4e34e
ci: pass the release branch through env to prevent shell injection
imantsk Sep 1, 2026
1da40c2
fix: flush versioned snippet cache on complete uninstall
imantsk Sep 1, 2026
badcd36
fix: clear all versioned cache groups on a snippet cache reset
imantsk Sep 1, 2026
847667c
fix: do not report a run-once snippet as executed in safe mode
imantsk Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ jobs:
GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }}
steps:
- name: Authorize labeler
env:
GH_TOKEN: ${{ github.token }}
run: |
actor='${{ github.event.sender.login }}'
perm=$(gh api "repos/${{ github.repository }}/collaborators/$actor/permission" --jq '.permission' 2>/dev/null || echo none)
Expand All @@ -116,6 +118,7 @@ jobs:
--ref main \
--field mode=generate \
--field repo="${{ github.repository }}" \
--field branch="${{ github.event.pull_request.head.ref }}" \
Comment thread
imantsk marked this conversation as resolved.
Outdated
--field pr_number="${{ github.event.pull_request.number }}" \
--field readme_path="./src/readme.txt"
echo "::notice::Dispatched changelog generation for PR #${{ github.event.pull_request.number }}"
36 changes: 30 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
# Changelog




## [3.10.2] (2026-09-01)

### Added
* Added a confirmation flow for run-once snippet execution and hardened the handler to prevent failed or confusing actions.

### Changed
* Snippet names now respect the row truncation Screen Option in the admin list for better readability.
* Version switching AJAX requests now validate the correct nonce, improving reliability when updating snippet versions.

### Fixed
* Fixed safe mode fatal errors caused by an undefined wp_get_current_user() call.
* Fixed PHP validation being triggered incorrectly when activating snippets in bulk.
* Fixed saving issues after a user session expires.
* Fixed warnings caused by aliased field names when reading modified snippet fields.
* Fixed vendor autoloader issues by stripping unprefixed namespace references.
* Resolved issues where snippet names and version switches could behave inconsistently in the admin UI.

## [3.10.1] (2026-08-28)

### Changed
* Enhanced active-link visibility in the toolbar with border-based styling
* Standardized sidebar label font sizing for consistency
* Enhanced active-link visibility in the toolbar with border-based styling.

### Fixed
* Fixed admin bar script loading on core to work correctly on front-end
* Fixed snippet modification dates to send with proper UTC offset
* Fixed row truncation screen option to persist correctly across sessions
* Fixed sticky sidebar from displaying unwanted horizontal scrollbar
* Fixed a fatal error affecting snippets that use a `namespace` or `declare` statement.
* Fixed the snippets page rendering blank when another plugin's screen settings filter returned an invalid value.
* Fixed snippet saving on hosts that block REST API `PUT` and `PATCH` requests, by sending writes as `POST` with a method override.
* Fixed the Snippets List Order setting not being applied to the snippets list.
* Fixed admin bar snippet scripts failing to load on the free version, including on the site front end.
* Fixed snippet modified dates being sent without the correct UTC offset.
* Fixed the row truncation screen option not persisting across sessions.
* Fixed inconsistent sidebar label sizes.
* Fixed the sticky editor sidebar showing an unwanted horizontal scrollbar.

## [3.10.0] (2026-08-24)

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.10.1",
"version": "3.10.2",
"main": "src/dist/edit.js",
"directories": {
"test": "tests"
Expand Down
6 changes: 3 additions & 3 deletions src/code-snippets.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
* License: GPL-2.0-or-later
* License URI: license.txt
* Text Domain: code-snippets
* Version: 3.10.1
* Version: 3.10.2
* Requires PHP: 7.4
* Requires at least: 5.5
*
* @version 3.10.1
* @version 3.10.2
* @package Code_Snippets
* @author Shea Bunge <shea@codesnippets.pro>
* @copyright 2012-2026 Code Snippets Pro
Expand All @@ -37,7 +37,7 @@
*
* @const string
*/
define( 'CODE_SNIPPETS_VERSION', '3.10.1' );
define( 'CODE_SNIPPETS_VERSION', '3.10.2' );

/**
* The full path to the main file of this plugin.
Expand Down
8 changes: 5 additions & 3 deletions src/css/common/list-table/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,11 @@
text-align: start;
}

// Snippet names always stay on a single line, truncating gracefully;
// the full name is exposed through the title attribute.
td.column-name > .snippet-name {
// Names and descriptions are both governed by the "Truncate long snippet
// names and descriptions" Screen Option. When it is off, the full name is
// shown; when on, it stays on a single line and the full value remains
// available through the title attribute.
&.truncate-row-values td.column-name > .snippet-name {
display: block;
max-inline-size: min(15rem, 30vw);
white-space: nowrap;
Expand Down
27 changes: 18 additions & 9 deletions src/js/components/ManageMenu/ManageMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'
import { __ } from '@wordpress/i18n'
import { createInterpolateElement } from '@wordpress/element'
import { fetchConstQueryParam, fetchQueryParam, updateQueryParams } from '../../utils/urls'
import { DismissibleNotice } from '../common/Notice'
import { DismissibleNotice, type NoticeType } from '../common/Notice'
import { SUBPAGES, Toolbar } from '../common/Toolbar'
import { UpsellPage } from '../common/UpsellDialog'
import { CommunityCloud } from './CommunityCloud/CommunityCloud'
Expand All @@ -24,31 +24,40 @@ const repositionTableOptionsSettings = () => {
}
}

const getNoticeText = (result: string) => {
const getNotice = (result: string): { text: string, type: NoticeType } | undefined => {
switch (result) {
case 'deleted':
return __('Snippet <strong>deleted</strong>.', 'code-snippets')
return { text: __('Snippet <strong>deleted</strong>.', 'code-snippets'), type: 'success' }

case 'executed':
return { text: __('Snippet <strong>executed</strong>.', 'code-snippets'), type: 'success' }

case 'run-once-failed':
return {
text: __('The snippet could not be run. Check that its code is valid and try again.', 'code-snippets'),
Comment thread
imantsk marked this conversation as resolved.
type: 'error'
}

default:
return undefined
}
}

const PageNotices = () => {
const [noticeText, setNoticeText] = useState(() => {
const [notice, setNotice] = useState(() => {
const result = fetchQueryParam('result')
updateQueryParams({ result: undefined })
return result && getNoticeText(result)
return result ? getNotice(result) : undefined
})

return noticeText
return notice
? <DismissibleNotice
className="code-snippets-notice"
onDismiss={() => {
setNoticeText(undefined)
setNotice(undefined)
}}
type="success">
<p>{createInterpolateElement(noticeText, { strong: <strong /> })}</p>
type={notice.type}>
<p>{createInterpolateElement(notice.text, { strong: <strong /> })}</p>
</DismissibleNotice>
: null
}
Expand Down
7 changes: 6 additions & 1 deletion src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ const RunOnceButton: React.FC<ColumnProps> = ({ snippet }) =>
<a
className="snippet-execution-button"
title={__('Run Once', 'code-snippets')}
href={buildUrl(window.location.href, { action: 'run-once', snippet: snippet.id })}
href={buildUrl(window.location.href, {
action: 'run-once',
snippet: snippet.id,
network: snippet.network ? 'true' : 'false',
_wpnonce: window.CODE_SNIPPETS_MANAGE?.runOnceNonce ?? ''
Comment thread
imantsk marked this conversation as resolved.
})}
>
<span className="screen-reader-text">{__('Run Once', 'code-snippets')}</span>
<span aria-hidden="true">&nbsp;</span>
Expand Down
4 changes: 3 additions & 1 deletion src/js/hooks/useRestAPI.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'
import axios from 'axios'
import { createContextHook } from '../utils/bootstrap'
import { REST_API_AXIOS_CONFIG, applyMethodOverride } from '../utils/restAPI'
import { REST_API_AXIOS_CONFIG, applyMethodOverride, applyRestNonce, listenForNonceRefresh } from '../utils/restAPI'
Comment thread
imantsk marked this conversation as resolved.
import type { PropsWithChildren } from 'react'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

Expand Down Expand Up @@ -60,7 +60,9 @@ const [Context, useRestAPI] = createContextHook<RestAPIContext>('useRestAPI')
export const WithRestAPIContext: React.FC<PropsWithChildren> = ({ children }) => {
const axiosInstance = useMemo(() => {
const instance = axios.create(REST_API_AXIOS_CONFIG)
instance.interceptors.request.use(applyRestNonce)
instance.interceptors.request.use(applyMethodOverride)
listenForNonceRefresh()
return instance
}, [])

Expand Down
5 changes: 3 additions & 2 deletions src/js/hooks/useSubmitSnippet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { __ } from '@wordpress/i18n'
import { isAxiosError } from 'axios'
import React, { useCallback } from 'react'
import { describeRequestError } from '../utils/errors'
import { useSnippetForm } from '../components/EditMenu/SnippetForm/WithSnippetFormContext'
import { createSnippetObject, isCondition } from '../utils/snippets/snippets'
import { buildUrl } from '../utils/urls'
Expand Down Expand Up @@ -113,8 +114,8 @@ export const useSubmitSnippet = (): UseSubmitSnippet => {
: api.update({ ...request, id }))

return response.id ? createSnippetObject(response) : undefined
} catch (error) {
return isAxiosError(error) ? error.message : undefined
} catch (error: unknown) {
return isAxiosError(error) ? describeRequestError(error) : undefined
} finally {
setIsWorking(false)
}
Expand Down
8 changes: 8 additions & 0 deletions src/js/types/Window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ declare global {
readonly wp: {
readonly editor?: WordPressEditor
readonly codeEditor?: WordPressCodeEditor
readonly hooks?: {
addAction: (
hookName: string,
namespace: string,
callback: (data: { rest_nonce?: string }) => void
) => void
}
}
readonly pagenow?: string
readonly ajaxurl: string
Expand Down Expand Up @@ -72,6 +79,7 @@ declare global {
cloudSearchPerPage: number
isSafeModeActive: boolean
bulkDownloadNonce: string
runOnceNonce?: string
supportsZipDownloads: boolean
editorTheme: string
}
Expand Down
33 changes: 33 additions & 0 deletions src/js/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,36 @@ export const unpackErrorResponse = (error: unknown): string => {

return __('An unknown error occurred.', 'code-snippets')
}

/**
* Explain a failed request in terms the reader can act on.
*
* An expired session is the common case worth naming: the snippet editor is a
* screen people leave open, and once the session lapses WordPress rejects every
* write with a 403 that says only "Cookie check failed". Reporting the raw
* status left people believing the plugin had ignored them.
*/
export const describeRequestError = (error: unknown): string => {
if (!isAxiosError(error)) {
return unpackErrorResponse(error)
}

if (!error.response) {
return __(
'The request did not reach your site. Check your connection, or whether a security plugin is blocking it.',
'code-snippets'
)
Comment thread
imantsk marked this conversation as resolved.
}

const data: unknown = error.response.data
const code = data && 'object' === typeof data && 'code' in data ? String(data.code) : ''

if ('rest_cookie_invalid_nonce' === code || 'rest_not_logged_in' === code) {
return __(
'You have been signed out, so nothing was saved. Sign in again in another tab, then save. Your changes are still here.',
'code-snippets'
)
}

return unpackErrorResponse(error)
}
47 changes: 46 additions & 1 deletion src/js/utils/restAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,54 @@ export const applyMethodOverride = (config: InternalAxiosRequestConfig): Interna
return config
}

/**
* The REST nonce to authenticate the next request with.
*
* Held in a variable rather than baked into the axios config, because the value
* the page was rendered with does not stay valid. A nonce expires with the
* session, and the snippet editor is a screen people leave open for a long
* time. Once it lapsed, every save failed with a 403 and the only cure was
* reloading the page, which loses whatever was being written.
Comment thread
imantsk marked this conversation as resolved.
*/
let restNonce = window.CODE_SNIPPETS?.restAPI.nonce

/**
* Keep the REST nonce current for as long as the page is open.
*
* WordPress already sends a freshly minted nonce with every Heartbeat response,
* from `wp_refresh_heartbeat_nonces()`. Core applies it to `wpApiSettings`,
* which our screens do not enqueue, so the value went unused. Listening for the
* tick ourselves means an editor left open stays able to save.
*/
export const listenForNonceRefresh = () => {
Comment thread
imantsk marked this conversation as resolved.
// Heartbeat also fires the tick through the hooks API, which avoids
// depending on jQuery being present and typed.
window.wp.hooks?.addAction(
'heartbeat.tick',
'code-snippets/refresh-rest-nonce',
(data: { rest_nonce?: string }) => {
if (data.rest_nonce) {
restNonce = data.rest_nonce
}
}
)
Comment thread
imantsk marked this conversation as resolved.
}

/**
* Attach the current nonce to an outgoing request.
*
* Read per request, so that a nonce refreshed since page load is actually used.
*/
export const applyRestNonce = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
if (restNonce) {
config.headers.set('X-WP-Nonce', restNonce)
}

return config
}

export const REST_API_AXIOS_CONFIG: AxiosRequestConfig = {
headers: {
'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce,
'Access-Control': window.CODE_SNIPPETS?.restAPI.cloud.token
}
}
Loading
Loading