Files
nolimit-discounts/ci/plugin.conf
T
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

87 lines
4.6 KiB
Plaintext

# The only file under ci/ that knows anything about this particular plugin.
#
# Everything else in ci/ is plugin-agnostic on purpose: copying ci/, tests/ and
# the two workflow roots into another plugin and rewriting these six lines is
# the whole port. docs/REUSE.md walks through it.
# Directory name the plugin gets inside wp-content/plugins, and the name of the
# built zip. WordPress identifies a plugin by "<slug>/<main file>", so changing
# this after release orphans every installed copy.
#
# 🔴 THIS DELIBERATELY DOES NOT MATCH THE REPOSITORY NAME, and it must not be
# "tidied up" to. The repo is woo-discount-manager; the installed directory has
# been woocommerce-email-discount-manager since the first release and is on a
# live shop. Renaming it would not move that installation — it would put a
# SECOND copy beside it and leave the old one active. The repo name is free to
# change (Gitea keeps a 301, measured), the installed slug never is.
PLUGIN_SLUG=woocommerce-email-discount-manager
# The file carrying the plugin header. Version: in here is the version.
PLUGIN_MAIN=woocommerce-email-discount-manager.php
# Every __() and esc_html__() call must use this domain or the translation is
# silently dropped at runtime.
TEXT_DOMAIN=wc-email-discount
# WooCommerce is installed into the test site before the plugin is activated.
# Set to no for a plugin that does not need it, and the site comes up faster.
NEEDS_WOOCOMMERCE=yes
# Run inside the live test site with `wp eval-file`, in this order. Fixtures
# create the data; the smoke files assert against it. A non-zero exit or an
# uncaught error fails the build.
SITE_TESTS="tests/fixtures.php tests/smoke.php"
# Plausibility floors for the hygiene checks. They exist because a check that
# examined NOTHING prints the same reassuring line as one that examined
# everything, and only the passing side can tell them apart — a pattern that is
# too broad fails loudly on first contact with real code, one that finds nothing
# survives verification forever.
#
# Both are deliberately far below the real numbers. They are not assertions
# about this plugin's size: a tight number goes red on ordinary churn and gets
# raised until it means nothing. They assert that the scan still finds this
# plugin at all.
#
# Real counts when these were set (2026-09-17): 13 shipped PHP files, 142
# translation calls across 17 files. Both floors were broken once and watched to
# fail before being believed.
MIN_SHIPPED_PHP_FILES=5
# Set to 0 for a plugin with no translatable strings: the check then warns
# instead of failing, so zero is never silent either way.
MIN_TRANSLATION_CALLS=60
# Where releases are published, when that is not the repository running the
# workflow. The source repo is kw/nolimit-discounts; the plugin's update feed
# is a separate PUBLIC repo holding built zips only, so a shop can fetch
# updates without credentials and without the source being readable.
#
# 🔴 This and the "Update URI:" header in the plugin file must name the same
# repository. ci/static.sh fails when they disagree — a feed that moves while
# the header stands still is how every installed copy silently stops receiving
# updates, and nothing announces it because latest_release() just returns false.
#
# Publishing here needs a token with write access to that OTHER repository. The
# job token only covers the repo it runs in, so this is a cross-repo PAT and
# Kristian's to add.
RELEASE_REPO=kw/nolimit-discounts-dist
# The sha256 of includes/shared-menu.php, which is byte-identical in all six
# plugins. Which copy a site loads is decided by activation order and PHP
# cannot un-declare a function, so the first one wins for the whole request —
# a shop with two plugins on one version and three on another gets whichever
# booted first, while every repository's own check is green on its own side.
# The frozen hash is therefore a family-wide fact, not a local one, and taking
# a new one is something all six do in the same window.
#
# 9012fe98 = contract 1.2.0. The glyph is now emitted as CSS numeric escapes
# rather than through esc_attr(): HTML entities are not decoded by the CSS
# parser, so esc_attr was protecting by coincidence — and a backslash, which it
# leaves alone, IS a CSS escape and swallowed the closing quote. Changing the
# alphabet beats filtering the input: what is emitted is nothing but
# backslashes, hex digits and spaces, so there is nothing left to leave the
# string with. Verified here against seven hostile inputs.
# Earlier frozen hashes: 184cebdc = 1.1.0, f4862d5c = 1.0.0.
WPMGR_MENU_SHA=9012fe98d867b86c993cee2f8d7b0b9fe5b89817fed4cf45346aa4949e2df307