Skip to content

Lay groundwork for the next release - #469

Draft
KevinBatdorf wants to merge 26 commits into
mainfrom
feature/dynamic-block-phiki
Draft

Lay groundwork for the next release#469
KevinBatdorf wants to merge 26 commits into
mainfrom
feature/dynamic-block-phiki

Conversation

@KevinBatdorf

Copy link
Copy Markdown
Owner

Long-running branch for the next release. Smaller pieces land here first and go to main together.

PR description generated by Claude.

KevinBatdorf and others added 26 commits August 21, 2026 21:51
The next major renders code server-side and needs mb_ereg_*. mbstring
built with --disable-mbregex has no ini switch and no add-on package to
fix it — only a different PHP build — and wordpress.org can gate updates
on PHP version but not on extensions. Left alone, those sites would
auto-update into a version they cannot render.

So the plugin holds itself back: a capability check behind the
blocks.codeBlockPro.canHighlight filter, and a site_transient_update_plugins
filter that drops this plugin's own entry when the check fails. Verified on
a playground site — with the check forced false the Plugins screen offers
no update, the Updates screen doesn't list the plugin, and the menu bubble
reads 0; other plugins are untouched. Note that playground's PHP does have
mbregex, so the editor-facing phases have to drive the filter seam rather
than the real capability.

Reading, not writing, is filtered on purpose. The stored transient still
carries the update, so nothing is lost if a later release can run here and
the gate comes off.

CI runs the suite twice, once with mb_ereg_search_init taken away by
disable_functions. No prebuilt PHP offers mbstring without mbregex, and
that ini setting reproduces the only part that matters: function_exists
returns false while the rest of mbstring keeps working. So one test covers
the unfiltered path for real in both directions, rather than trusting the
filter seam to stand in for a build nobody can install.

This also stands up composer and phpunit, which the repo had neither of —
tests run against a WordPress checkout named by WP_ABSPATH, with a
dedicated database so they never touch a working install. Requires PHP
stays at 7.0: bumping it would stop this release reaching the very sites
it exists for.

Co-Authored-By: Claude <noreply@anthropic.com>
Hiding mb_ereg_search_init with disable_functions made the capability
check false, but it was still a stand-in: mbstring kept the rest of the
mb_ereg family, and nothing proved the plugin behaves on the build this
release exists for. No prebuilt PHP ships mbstring without mbregex, so
the only way to get one is to compile it.

The Dockerfile builds PHP with --enable-mbstring --disable-mbregex and
without libonig, so a build that quietly keeps mbregex fails instead of
passing. Confirmed locally on 8.4.24: mbstring loads, mb_substr works,
the whole mb_ereg family is gone, and phpinfo no longer reports an
oniguruma version. WordPress installs and the suite passes, and with the
transient filter removed the unfiltered test fails — so it is reading the
build, not a filter.

The image is pushed to GHCR and the second job runs inside it. Public
packages cost nothing and don't expire, unlike Actions cache entries,
which are evicted after a week idle and would silently recompile. Both
jobs live in one workflow so the image is built before the job that
pulls it, and the database now comes from the service's own MYSQL_DATABASE
rather than a client call, since the image carries no mysql client.

The mbregex-absent job talks to the service by hostname because a job
running in a container can't reach it on 127.0.0.1.

Co-Authored-By: Claude <noreply@anthropic.com>
The gate makes the plugin go quiet on servers without mbregex, which on its
own reads as a plugin that simply stopped getting updates. The capability now
rides along with the editor's inline script, next to pluginUrl, and the Theme
panel carries a notice naming mbregex and the mbstring extension so a site
owner has something concrete to take to their host. The check stays in PHP —
the editor is told the answer rather than working it out, so the notice can't
disagree with the update gate.

The notice states a requirement and offers nothing to click. Letting the
update through anyway would leave every new code block rendering without
colors, which is a broken site rather than a choice worth putting in front of
someone; and hiding the notice would take away the only explanation the site
has for a plugin that no longer updates. It stays until the server can
highlight.

Playground's PHP has mbregex and no build without it can run there, so the
spec installs an mu-plugin that turns the capability off for requests
carrying cbp_no_mbregex — the filter seam the gate left for exactly this. The
absence case waits for the Manage themes button first, since the panel is
empty until the settings store hydrates and a missing notice would prove
nothing. Confirmed the other direction is real: with the component left out
of the bundle, the notice test failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropping our entry from the update transient only removes the offer. wp-cli
and the update screens hand a package straight to the upgrader, which never
reads that transient, so `wp plugin update code-block-pro` would still put a
version this server cannot render onto the site.

upgrader_pre_install returns a WP_Error for our own basename when the
capability is missing, which aborts install_package before anything is
copied over the live plugin. wp-cli prints the message and the update screen
shows it in place of a success line, so the refusal is visible rather than a
silent no-op.

Two paths stay open, because the upgrader can't be told what they carry: an
install action names no plugin in hook_extra, so `wp plugin install --force`
and files copied in over SFTP both look like any other install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notice only knew about mbregex, so a site running PHP 8.1 with mbregex
present was told nothing at all: wordpress.org quietly stops offering a
version whose Requires PHP it can't meet, and the editor stayed silent about
it. The editor now receives hasNextPhp and nextPhp alongside canHighlight,
and the notice picks between three messages so it names what this particular
server lacks — mbregex, the PHP version, or both. 8.2 is written once, in
code_block_pro_next_php, and reaches the copy through a placeholder.

The gates themselves stay mbregex-only. Core reads Requires PHP out of the
package and refuses the install on its own, wp-cli included, so holding
1.x back over a version would take away updates core would have allowed
through. Two tests pin that separation.

Playground runs 8.5 with mbregex, so the mu-plugin the spec installs now
fakes both, one request parameter each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The messages opened on the requirement and read like a failure report. They
now open on version 2 arriving and keep one sentence for what this server
lacks, which is the part a site owner repeats to their host. The wrapper
carries vertical margin so the notice isn't wedged between the panel header
and the theme search.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wrapper carried my-6 while the utilities in this build are emitted as
`.code-block-pro-editor .my-6`, so the class only styles descendants and the
computed margin came out 0. Moved onto an inner div, which measures 24px top
and bottom. An emoji goes with the announcement of version 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor fell back to a hardcoded 8.2 when the localised value was
missing, so a later bump would have left the notice quoting a version the
check no longer used. The version now only ever comes from PHP, and a notice
with no version to name treats the PHP requirement as unmet rather than
guessing at it. Splitting the three messages apart drops the flag-shaped
argument that decided between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An install action carries no plugin in hook_extra, so uploading the zip or
`wp plugin install --force` walked straight past the refusal that covers
updates. upgrader_source_selection sees the unpacked directory instead, so
the package can be identified from its own headers: any php file in its root
declaring our text domain means it is us, whatever the file or folder is
called. The test builds a package named entry.php for that reason.

Files copied in over SFTP still can't be caught — no WordPress code runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mbregex compiles in or out as a unit, so one probe answers for the family on
any normal build. disable_functions doesn't work that way: a host can hide
mb_ereg_search_getregs and leave mb_ereg_search_init in place, and the gate
would wave the update through to a site that fatals on render. The check now
covers all four functions phiki calls, and a test pins the list so a
narrowing goes noticed.

Blocking a capable site wrongly only leaves it on 1.x. Letting an incapable
one through breaks its pages.

The announcement draft names the same four.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list is checked against itself, and phiki is not a dependency of this
repo, so nothing here would notice phiki adding a fifth call. Naming the
version at least says when it was last true.

Verified against phiki v2.2.1: those four are every mb_ereg_* it calls, and
the plain mb_strlen/mb_strpos/mb_substr it also uses come with mbstring
whether or not mbregex was built in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
can_highlight() said something untrue about a working site: 1.x renders in
the editor through wasm, so a server without mbregex highlights perfectly
well and would still have been told it could not. The question the gate
actually answers is whether this server can take the next version.

The filter and the window.codeBlockPro key move with it. Neither has
shipped, so nothing outside the repo is holding the old names. The test
names follow the same wording, and highlighting_functions() keeps its name
because that list really is what highlighting needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list had one real caller and a test that asserted it against itself,
which catches nothing phiki could actually do. Inlined into the only place
that reads it; the build-decides test now names one function directly, so
it still runs for real on both CI legs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both functions existed to feed two JSON keys. The version is a constant, and
the comparison is one expression at the only place that reads it, so the
gate file now holds nothing but the gate.

The two PHPUnit tests asserting PHP does not gate updates went with them —
with the comparison out of update-gate.php there is no longer a filter for
them to turn, and the notice test still drives the same filter through
cbp_old_php against a real render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…orth it

Two call sites and no logic; plugin_basename(__FILE__) in the entry file
says the same thing without a function to look up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gate is for people who click update without reading and for sites on
auto-updates. Both of those read site_transient_update_plugins, so hiding
our entry there is the whole job.

upgrader_pre_install only added wp-cli, and anyone driving wp-cli knows
what they are doing. upgrader_source_selection only added a hand-uploaded
zip, which needs someone to visit wordpress.org and download it on purpose
after being told in the editor why updates stopped — and core already
refuses that upload on the PHP requirement by itself.

That takes out fourteen lines of package sniffing, a duplicate error, and
seven tests covering paths we no longer block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
About 39% of WordPress runs below PHP 8.2, so on ten thousand installs this
notice would have appeared in a few thousand editors, undismissable, from
release day until 2.0 actually lands. Nobody on PHP 7.4 can act on it
quickly, and core already refuses 2.0 on Requires PHP at the moment they
try to install it, with its own message.

What is left is the mbregex case, which has no other surface: core cannot
see a missing extension and would let the install through.

The payload keys, the types and two of the notice specs go with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gate hid our entry from the update list whenever mbregex was missing,
with no regard for what was on offer, so a site would have stopped at the
version that first refused one — including every later 1.x fix. On ten
thousand installs, with no telemetry to ever tell us it happened, that is
not a trade worth making for a case we have never observed.

It now reads new_version and stands aside for anything below 2.0. An offer
carrying no version is left alone rather than guessed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit added the version comparison but the constant never
landed in the entry file, so every admin page fatalled on admin_init as
soon as mbregex was reported missing. Caught on a real install, not by
phpunit, which had not run yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reaching it meant having a code block, selecting it, opening the sidebar
and expanding the Theme panel, which is collapsed by default. For the only
explanation a site gets of why its updates stopped, that is four steps too
many.

It now sits directly under the block card, in the slot the pack's own
action-required notice already uses, so the specs no longer open a panel to
find it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every neighbour in the inspector is edge to edge with a one-pixel border.
The notice had a 16px margin, so it floated with air above and below while
nothing else did. It now takes the padding inside and a bottom border, and
butts up against the panel below it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fills land in the order their components mount, and the block's own sidebar
mounts innermost, so every other plugin's panels were rendering above the
notice — Animation and TranslatePress on a normal install. Being the first
child of our own InspectorControls bought nothing.

A separate editor.BlockEdit filter at priority 99 is applied last, which
makes it the outermost wrapper, so it mounts first and its fill registers
before theirs. The notice now sits directly under the block card with the
other panels below it.

The content group would also have put it there, but filling that group adds
a Content tab to the inspector and hides every setting behind the other one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wrapper's own horizontal padding stacked on the notice component's, so
the text sat 32px from the sidebar edge while the block description and
every panel label sat at 16px. Dropping the side padding lets the notice go
full width, where its 12px padding plus 4px status bar land the text at 16px
exactly. The right inset needed the extra 4px to match.

Measured: block description 1016..1264, notice text 1016..1264.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The constant had one reader, thirteen lines away in another file, and it
sat directly under the plugin header's Version. Bumping it while cutting
2.0 would look like housekeeping and would silently switch the gate off
for the one release it exists to gate.

It is not a version that tracks anything; it is a fixed claim about which
release needs mbregex. With the number inline, the comment no longer has
to name it, only the constraint it protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The block is about to change shape — new attributes, a render callback,
a different highlighter — and every block already saved on a real site
has to survive it. These are 50 of them, serialized by the 1.28.0 editor
itself, so the markup is whatever WordPress writes rather than what a
hand-minted fixture guesses: the escaping in the comment JSON, the exact
style attribute, the empty wrapper an empty block gets.

The set covers what the rebuild can break, not one block per language:
the trailing newline that makes 13 span.line out of 12 lines, indices
pointing past the last line, every header, footer, see-more and copy
variant, real tabs against absent tab attributes, the legacy pixel
gutter, a language and a theme that no longer resolve, the pack's
CSS-variables theme, and CRLF, unicode and shortcodes in the source.

tests/legacy-blocks asserts the two things that must hold: each block
still parses valid in the editor, and the front end still renders the
same text, line count and glyph colours. It compares a digest rather
than markup, because phiki will legitimately emit different classes and
move colours to a stylesheet — a changed glyph colour is the regression.

The generator runs only under GENERATE_FIXTURES and is skipped otherwise.
It has to stay unused once save() changes, or it would start producing
the very output the set exists to compare against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant