Hold back updates where mbstring lacks mbregex - #467
Draft
KevinBatdorf wants to merge 25 commits into
Draft
Conversation
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>
KevinBatdorf
force-pushed
the
feature/mbregex-gate
branch
from
August 22, 2026 03:53
028f5b8 to
ba14f1c
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Some servers run PHP without a piece of text-matching that the next big release needs to colour code. Site owners can't add it — that takes a different PHP build — and WordPress.org can't hold an update back over it. So the plugin looks at the server it's on, stops offering itself an update when it wouldn't be able to render, and says so in the editor. Sites that are fine see no difference.
wp plugin updateand a hand-uploaded zip still go through. Driving wp-cli, or downloading the zip from wordpress.org on purpose after the editor has told you why updates stopped, is a deliberate act by someone who knows what they are doing.PR description generated by Claude.