9931c74b459ee3db8f9f6359772e7e78dd61f73b
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
cb58ffe26e |
1.5.4: the text follows the rename, and a check keeps it that way
Kristian read the readme and found it still telling people to configure the plugin under WooCommerce, for a page that moved to the Nolimit menu three releases ago. Sweeping for it found seven stale places, and three of them are notices THE PLUGIN SHOWS THE USER — the missing-WooCommerce notice, the activation error and the incomplete-install warning all still called it WooCommerce Email Discount Manager. That is the mimir.guide failure exactly: the code moved, the text stood still, and nothing failed. It was found by somebody reading, which is the only way it could be found, and that is the argument for the check rather than for being more careful next time. ci/static.sh now asserts two things. The name in readme.txt's title matches the Plugin Name header, because the header is what WordPress shows and everything else has to agree with it. And no shipped PHP file contains OLD_PLUGIN_NAME, which is now a value in ci/plugin.conf rather than a habit. The changelog is deliberately exempt: an entry describes what was true for that release, and rewriting it would be a different lie. Only text describing the CURRENT state is checked. Both halves proven red — a readme title that disagrees, and the old name put back into one notice. Not changed: the page stays under Nolimit rather than gaining a second entry under WooCommerce. One menu is the family's shape, and a duplicate entry is clutter rather than a fix for a readme that was simply wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ae4112c773 |
Publish releases to kw/nolimit-discounts-dist and point the header there
The family shape is source private, releases in a separate public repo so a shop can fetch an update without credentials and without the source being readable. This plugin was the only one still serving its own update feed out of the repository that holds ci/, docs/ and tests/. kw/nolimit-discounts-dist created (public, zips only). RELEASE_REPO in ci/plugin.conf now names it, and ci/publish-release.sh honours it over GITHUB_REPOSITORY — falling back to the running repo, which is what a plugin without a split still wants. 🔴 The "Update URI:" header moves in the SAME commit as the publishing target, and ci/static.sh now fails when the two disagree. The header is what every installed copy polls; if it names a repository nothing is published to any more, the shop stops being offered updates and nothing announces it, because latest_release() simply returns false. That is the same failure as a privacy page standing still while the code moves, and the check is what makes it impossible to forget. Proven red before being believed: pointing RELEASE_REPO at another repository fails with both sides named. RELEASE_REPO is required by ci_load_conf, because an unset value would turn the comparison into a no-op and restore exactly the bug it prevents. Not done here, and it is the blocker for actually cutting a release: publishing to another repository needs a token with write access to it. The job token only covers the repo it runs in, so this needs a cross-repo PAT in the repository secrets, and that is Kristian's to add. Also not done, deliberately: the source repo stays public for now. Making it private before the dist repo has releases would break the feed for the copy installed on the live shop, whose header still points at the old address. 1.4.6 and earlier keep working through Gitea's rename redirect - measured, the old name still answers 200 - and that redirect is the only thing holding them up, so it must not be removed until a build carrying the new header has propagated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9b0b0ab475 |
Make the CI checks say how many files they actually read
Four checks in ci/ printed a reassuring line having examined nothing. Found independently by the woo-ai-manager and woo-cache-manager sessions in their own copies of this kit; all four reproduce here. A check that examined nothing prints the same sentence as one that examined everything, and only the passing side can tell them apart. That is why these survived being verified: a pattern that is too broad fails loudly the first time it meets real code, while one that finds nothing passes forever. So every success line now carries a count — the old text domain line could not tell 142 from 0. - static.sh, direct-access guard: counts what the loop examined and fails below MIN_SHIPPED_PHP_FILES. shipped_files() swallows a dist-files.sh failure with `|| true`, so an empty list is one broken .distignore away, and this is the check standing between a PHP file and being requested straight from a browser outside every capability check the code assumes has happened. The floor is above 1 because a pattern that wrongly excludes includes/ collapses the list without emptying it. - static.sh, credential scan: counts what it read, fatal on zero. A pushed key is compromised even after the commit is deleted, so a silent no-op here is not a weak check but the absence of one wearing a green tick. The floor is 1 and is not configurable: this list has no filter that could go partially wrong, so the only way it misreports is by coming out empty. - check-textdomain.php: three zeros, three verdicts. No PHP files at all is always fatal and deliberately not caller-configurable — it says the scan root is wrong, not that the plugin is small. Below --min is fatal. Zero calls with no --min is a warning, since a small plugin may legitimately have no translatable strings, but it is never silent. - check-textdomain.php: fix the annotated path. str_replace( $root, '', $path ) with the default root of "." removed every dot anywhere in the path, so a real error in includes/class-db.php was reported on "includes/class-dbphp" — not a file, not clickable, and not greppable. Every error this script printed with the default root was mangled that way. The floors live in ci/plugin.conf, the only file under ci/ that knows anything about this plugin, rather than hardcoded in static.sh: static.sh is copied verbatim between plugins too, so a number in it would make this plugin's shape everybody's constraint. ci_load_conf now requires them — an unset floor turns its comparison into a no-op and silently restores the bug. Each floor was broken once and watched to fail before being believed: empty shipped list, wrong scan root, --min above the real count, zero calls without --min, empty credential list, and a plugin.conf missing both floors. Static checks green, and now report 13 shipped PHP files, 142 translation calls across 17 files, 44 tracked files read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f14c51dcf2 |
Rename the repository to woo-discount-manager, keep the installed slug
Kristian named the five plugins as a family, and three repos disagreed with what
he calls them. His decision: rename the repo, never the installed slug.
MEASURED BEFORE DOING IT, because four published releases carry the old URL in
their Update URI header. A throwaway repo was created, renamed and deleted:
old API path 301 -> new
old web path 301 -> new
old /releases/download/... path 301 -> new
and WordPress follows up to 5 redirects by default (class-wp-http.php:191), on
GET. Confirmed afterwards against the real thing: the old feed URL resolves to
v1.4.5 and the old download path still serves 48185 bytes.
So a shop on 1.4.2 keeps updating through the redirect, and 1.4.6 moves it onto
the new address directly.
PLUGIN_SLUG and PLUGIN_MAIN are deliberately untouched and now disagree with the
repository name. That is permanent: the installed directory has been
woocommerce-email-discount-manager since the first release and is on a live
shop, and changing it would not move that installation — it would put a SECOND
copy beside it and leave the old one active. ci/plugin.conf says so in the file,
because otherwise somebody will helpfully align them.
Also in, both for the family rather than for this plugin alone:
- docs/REUSE.md gains the hostname-keyed hook as a first-class section. It is
the only bug we have found that is CAUSED by there being more than one plugin,
so it is invisible to anyone reading a single repo — along with the same shape
in a shared-*.php file copied into five plugins, which fatals with "Cannot
redeclare" on the second activation without function_exists() guards.
- ci/static.sh fails on anything shaped like a credential in a tracked file.
Suggested by the AI-manager port: a pushed key cannot be unpushed, only
rotated. Deliberately narrow — sk-ant-, sk-, ghp_, gho_, github_pat_, AKIA,
PRIVATE KEY blocks — because a broad entropy heuristic fires on minified
assets and a check that cries wolf gets commented out.
Verified failing before being believed: a file containing sk-ant-api03-... makes
the step report the filename and the suite exit 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
eca2851d35 |
Test against the newest WordPress and WooCommerce, and ship updates
Three things this repository could not do: prove the plugin still works on a
WordPress or WooCommerce released after it, get a fix onto a shop without
someone uploading a zip, and do either of those for the next plugin without
starting over.
ci/ is the test harness. ci/site-test.sh builds a throwaway WordPress +
WooCommerce in containers at any versions you name — including `nightly` and
`beta` — installs exactly the files the release zip will contain, and runs
tests/smoke.php inside it with wp eval-file. Real cart, real coupons, real
WC_Shipping_Rate objects: a mocked cart keeps passing after WooCommerce has
changed underneath it, which is the one failure worth catching here. WP_DEBUG
is on and any notice logged from this plugin's directory fails the build,
because that is the shape an upgrade actually breaks in — nothing fails, a
notice appears, and two releases later it is a fatal.
compat.yml runs that weekly against today's releases, the next WordPress and
the next WooCommerce, the oldest PHP the header promises and the newest PHP.
When it is green it opens a PR bumping "Tested up to" and "WC tested up to" —
only when green, because WooCommerce shows that second value to the shop owner
as an untested-plugin banner, and that banner is what they read before deciding
whether to press update.
includes/class-updater.php is the distribution half. It hooks
update_plugins_{hostname} for the host in the new "Update URI" header and reads
the Gitea release API, so WordPress handles the notice, the download, the
"Enable auto-updates" link and wp plugin update with no further code. Update
URI earns its place twice: since 5.8 a plugin that declares one is never asked
about on wordpress.org, so a .org plugin sharing this slug cannot be pushed
onto a shop running this one. The check sends nothing about the site and
overrides WordPress's default user agent, which carries the site's own URL.
release.yml tags, tests, builds, publishes — then reads the feed back with no
credentials. A private repository and a release with no zip attached both leave
a page that looks perfect to anyone logged in and an update no shop is ever
offered.
Measured before committing, all green: WP 7.1 + WC 11.1 on PHP 8.3 and 7.4,
WP 7.2-alpha on 8.3, and the beta cell falling back to stable on 8.4. No
deprecations from this plugin on any of them.
Three guards are there because the bug they catch is invisible without them:
the annotated tag is re-fetched before its message is read (checkout turns it
into a lightweight tag pointing at the merge commit), the two workflow roots
are kept in sync (.gitea/workflows hides .github/workflows entirely), and every
file the code loads must be in the release zip — which caught class-updater.php
being referenced and uncommitted while every local test passed.
docs/REUSE.md is the port to the next plugin: copy ci/, rewrite five lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|