Commit Graph
6 Commits
Author SHA1 Message Date
kristianwindandClaude Opus 5 8143d146e4 1.6.1: shared-menu 1.2.3, and render the dashboard to check the pill
The status pill on the shared dashboard is white 11px text over a light
overlay. SEO measured it two ways, arithmetic and a canvas read-back in
headless Chromium: this plugin's #1a7a52 gave 3.50:1 against a 4.5 minimum,
and six of eight family colours failed along with the no-colour fallback.

Not fixable by choosing a different card colour. The header is a direct pair
and passes at 5.32:1; the pill is a COMPOSITE, and 22% white is a floor — a
card has to reach about 7.7:1 before the pill clears 4.5, which is above the
whole family band. So it is the shared file or seven plugins giving up the
colour the cards exist to carry. 1.2.3 makes the overlay rgba(0,0,0,.18) and
this plugin's pill goes to 7.15:1.

Verified before copying: the hash, and php -w over both files to confirm the
executable code really changed — the diff is the version constant and that one
CSS value, nothing else.

The new assertion renders the dashboard and reads the pill out of what came
back, and it is built around the trap SEO warned about: their first harness
printed "no problem" over a code path it never entered, because it stubbed
wpmgr_cards instead of wpmgr_dashboard_cards and the renderer took its early
return. So the card's own title is asserted first — if the renderer bailed,
nothing after it means anything — and the OLD rgba is asserted ABSENT, because
a check that cannot tell the two versions apart cannot testify about either.

Both proven: against 471b0a2a it fails naming "the old 22% white", and with the
card filter removed it fails at 81 bytes rendered. That 81 is the same number
SEO reported from their own broken harness, which is independent confirmation
it is the same code path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 17:21:44 +02:00
kristianwindandClaude Opus 5 6a789349ec 1.5.3: shared-menu 1.2.2, and run the glyph style instead of reading it
678f1f03 is withdrawn. It hard-coded the icon box at 36x34, and WordPress
declares that parent three times with different sizes — 36x34 open, 35x30
folded, 34x30 under 960px — so a fixed number is correct in one state and 4px
too tall in the two a laptop is usually in. cache-manager measured where the
ink lands rather than where the box is and found it worse than that: the glyph
moved from 2.5px above centre to 2.5px below in the folded states, the same
error mirrored, in a state that had been nearly right. 1.2.2 removes the number
entirely with width/height at 100%.

1.5.2 is already published with 678f1f03, so this tree shipped the withdrawn
version. Checked before anything else, because a later candidate fatalled on
every admin page with "Unknown format specifier": that file was never taken
here, and the published 1.5.2 emits its stylesheet cleanly — verified by
running sprintf over its format string rather than reading it. So the shop is
cosmetically off in folded menus, not broken.

New assertion, and it is the one this round is really about: the style
function is INVOKED and its output captured, rather than the source being
read. The fatal that prompted it survived three sessions reading the file,
a clean php -l, a clean PHPCS, and two browsers agreeing about the CSS —
because every one of those was fed the stylesheet by hand rather than through
the printf that produces it. A literal per-cent must be doubled in a format
string, and nothing that looks at the file can see that.

Proven red with that exact fault rather than an arbitrary one: undoubling the
per-cent gives "ValueError: Unknown format specifier ';'".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 19:07:44 +02:00
kristianwindandClaude Opus 5 6114473be0 1.5.2: take shared-menu 1.2.1, which centres the glyph
Kristian reported the infinity sign sitting above centre in the admin menu on
the live shop. Security traced it to WordPress's own stylesheet rather than
adjusting it by eye: #adminmenu div.wp-menu-image is 36x34 and its :before
carries padding 7px 0, which is exactly 7 + 20 + 7 for a 20px dashicon at
line-height 1. The 1.2.0 rule set line-height 1.4 and inherited that padding,
so the pseudo-element was 42px tall inside a 34px parent.

1.2.1 reproduces the box, drops the padding and centres with flex — geometry
rather than a nudge, because U+221E sits on the maths axis instead of filling
its em box.

It is the same half-done abstraction as the escaping fix: the filter made the
character configurable and left the geometry tuned for one specific character.
Nothing could fail, because the only value anyone ever passed was the one it
was tuned for.

Verified before copying, including the part that makes the check meaningful:
Security predicted php -w WOULD differ this round, since the executable code
changed — so equal stripped hashes would have meant the wrong file. They
differ, and the diff is the version constant plus the CSS geometry, with
wpmgr_css_escape untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 18:52:51 +02:00
kristianwindandClaude Opus 5 f908771f33 Take shared-menu.php 1.2.0: the glyph is CSS-escaped, not esc_attr'd
Frozen by Nolimit SEO as 9012fe98, and it fixes something found here: the 1.1.0
glyph went through esc_attr() into a <style> element, where HTML entities are
not decoded by the CSS parser. It was protecting by coincidence rather than by
working, and Security found the case where the coincidence stops covering — a
backslash, which esc_attr leaves alone, is a CSS escape and swallows the
closing quote.

No injection was possible on any input any of us could construct, so this is
hardening rather than a hole, and the value comes from plugin code rather than
from a request.

The fix changes the alphabet instead of filtering the input: the glyph is
emitted as CSS numeric escapes, so what reaches the stylesheet is nothing but
backslashes, hex digits and spaces. cache-manager's statement of why that is
the right shape is the one worth keeping — the value CANNOT CONTAIN A
DELIMITER. "It escapes correctly" is a description, and a whitelist claims the
same description while needing to be kept honest against inputs nobody has
thought of yet.

Verified here rather than accepted:

- sha256 matches 9012fe98 before copying.
- The "b8d9284b plus two full stops and a phpcs pair" claim is comment-only,
  measured with php -w over both: one sha256 for the stripped source.
- The escaper itself run against seven inputs including a bare backslash, a
  quote, a CSS breakout, an HTML breakout and a NUL, asserting the output
  matches nothing outside [backslash, hex, space]. All seven clean.
- Correct codepoints, and no mb_* calls, so the hand-written decoder holds on
  installs without mbstring — which is every install none of us test on.

WPMGR_MENU_SHA updated in ci/plugin.conf. SEO's instruction said ci/static.sh,
which is their layout, not this one — following it literally would have set a
variable nothing reads, green on both sides while the check still asserted the
old value. Found by searching for the OLD VALUE rather than the path: zero
occurrences of any earlier frozen hash remain anywhere in ci/, tests/ or either
workflow root, and the new one appears exactly once.

Then proven consumed in both directions, which is the assertion that catches a
copy nothing reads: changing the file by one byte moves "got", and changing the
value in plugin.conf moves "expected".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:52:40 +02:00
kristianwindandClaude Opus 5 3020338378 Take shared-menu.php 1.1.0 and keep its hash in one place
The family froze a new copy of includes/shared-menu.php: 184cebdc, contract
1.1.0. Two changes in it for this plugin, which was still on f4862d5c (1.0.0).

The menu icon is now an infinity sign, painted from admin_head behind a
wpmgr_menu_glyph filter that defaults to it — Kristian asked for it because the
family is called Nolimit. And the file's own header said 1.0.0 while its
constant said 1.1.0, and it counted five plugins where there are six; Nolimit
SEO found both.

Taken today rather than when convenient, and the reason is measured rather than
tidiness: two plugins were on 1.1.0 and three on 1.0.0 at the same time, so a
shop got the glyph or the cog depending on activation order — while every
repository's own hash check was green on its own side. A frozen file is a
family-wide fact, and a green local check says nothing about the shop.

The API change is additive — a new filter and a new function, nothing removed —
so this plugin's use of wpmgr_ensure_menu() and wpmgr_menu_title is unaffected,
and the default glyph means there is nothing to opt into.

The hash now lives once, as WPMGR_MENU_SHA in ci/plugin.conf, checked by
ci/static.sh and required by ci_load_conf so an unset value cannot turn the
comparison into a no-op. It was previously a literal inside tests/smoke.php as
well; two copies of one constant drift, and the copy that drifts is the one
nobody remembers. The runtime assertion that remains proves a different fact —
that the file is loaded at all — because wpmgr_ensure_menu() only exists if it
was.

Proven red by appending one line to the shared file: the check names both
hashes and says not to edit it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:31:37 +02:00
kristianwindandClaude Opus 5 9f19688900 Rename to Nolimit Discounts and move the page under the shared menu
Kristian could not find this plugin in the Nolimit menu, and it still called
itself WooCommerce Email Discount Manager. Both are the display side of the
family rename; neither touches the installed folder.

Plugin Name is now "Nolimit Discounts". The folder, the text domain, the option
prefix and the three tables are deliberately unchanged: WordPress identifies a
plugin as <folder>/<file>, so renaming the folder would not move the
installation on the live shop — it would put a second copy beside it and leave
the old one active. That is a migration with a shim, and it is Kristian's call
rather than a side effect of a label change.

includes/shared-menu.php added, byte-identical to the family copy
(f4862d5c…c63185), and the page moved from the WooCommerce submenu to the
shared parent. The page slug is unchanged, so admin.php?page=… still resolves
and an existing bookmark keeps working — moving a page between parents does not
move its URL.

The label is claimed through the wpmgr_menu_title filter from this plugin's own
bootstrap, NOT by editing the shared file. Which copy of that file a site loads
is decided by activation order, and PHP cannot un-declare a function, so the
first copy wins for the whole request: editing it would make a site with three
of five plugins updated show "Manager" or "Nolimit" depending on who booted
first, and change its mind when a plugin is deactivated. The filter is
idempotent and a plugin still on an older build changes nothing.

The filter is registered before the WooCommerce gate, for the same reason the
updater is: the shared menu's label must not depend on whether this particular
plugin got past a check.

Two assertions, both proven red first. The label is asserted through the filter
rather than by reading the menu, because the menu is only built in an admin
request and the smoke tests run under WP-CLI. And the shared file's sha256 is
asserted, so editing it goes red here rather than going strange on a shop —
appending one comment line produced:

  FAIL  and the shared menu file is the family copy, unedited
        — expected 'f4862d5c…', got '45df6b55…'

Also caught in passing by the kit, working exactly as intended: the new
shared-menu.php was untracked, and "every file the plugin loads is a file the
release contains" failed rather than shipping a zip without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 22:05:19 +02:00