From 817690c1b6ec2118ddcf3b8cfe8bc5ea7f3933b3 Mon Sep 17 00:00:00 2001 From: peptiderepo <273788462+peptiderepo@users.noreply.github.com> Date: Sat, 13 Jun 2026 05:42:13 +0000 Subject: [PATCH] fix(schema): rewrite sanitize_molecular_formula() with element-grammar (Option B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace character-allowlist regex with an element-token grammar validator. Strip control characters first, then extract the maximal leading run of valid element tokens ([A-Z][a-z]?\d*) and group separators (()[]·.). Fixes: 'C10H12\ninjection' now returns 'C10H12' (was 'C10H12injection'). New: fully invalid inputs (e.g. '') return ''. Keep: parenthesised formulas (e.g. 'C2H5(OH)') pass unchanged. Unit tests expanded from 4 to 7 formula cases covering all five DoD requirements plus whitespace-only edge case. PHP not installable in sandbox; all 7 test inputs hand-traced against the regex and confirmed correct via Python simulation. Agent-Session: peptide-schema-backfill v0.6.0 formula-validator --- CHANGELOG.md | 1 + .../cpt/class-pr-core-schema-sanitizers.php | 54 +++++++++++++++++-- tests/unit/test-migration-0004.php | 26 ++++++++- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea89eab..7c433ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Format: [Semantic Versioning](https://semver.org/). ### Fixed - `tests/unit/test-migration-0004.php`: `error_log()` stub now guarded with `function_exists()` to prevent PHP fatal redeclaration error in CI PHP Lint (8.1/8.2/8.3). - `tests/bootstrap.php`: `wp_json_encode` stub updated to accept `int $flags = 0` second argument, matching production call signature; guarded with `function_exists()`. +- `cpt/class-pr-core-schema-sanitizers.php`: `sanitize_molecular_formula()` rewritten from character-allowlist to element-formula grammar validation (Option B). Now strips control characters first, then extracts the maximal leading run of valid `[A-Z][a-z]?\d*` element tokens and group separators. Inputs containing trailing English words (e.g., `"C10H12\ninjection"`) correctly return the formula prefix only (`"C10H12"`); fully invalid inputs (e.g., `""`) return `""`. Parenthesised formulas (e.g., `"C2H5(OH)"`) preserved unchanged. Seven-case unit test coverage added. ### Technical - Schema version: `PR_CORE_TARGET_SCHEMA_VERSION` = 4. diff --git a/includes/cpt/class-pr-core-schema-sanitizers.php b/includes/cpt/class-pr-core-schema-sanitizers.php index 641741d..8d9b24f 100644 --- a/includes/cpt/class-pr-core-schema-sanitizers.php +++ b/includes/cpt/class-pr-core-schema-sanitizers.php @@ -17,18 +17,62 @@ declare(strict_types=1); */ class PR_Core_Schema_Sanitizers { + /** + * Regex token for a single element symbol + optional count (e.g., "C10", "Na", "O"). + * + * Matches: one uppercase letter, optionally followed by one lowercase letter, + * optionally followed by one or more digits. + * + * @var string + */ + const FORMULA_TOKEN = '[A-Z][a-z]?\d*'; + + /** + * Regex character class for allowed group-separator / structural characters. + * + * Covers: parentheses, square brackets, interpunct (·, U+00B7), period, digits. + * These may appear between element tokens in complex or hydrated formulas. + * + * @var string + */ + const FORMULA_SEP = '[().\[\]\xB7\d]'; + /** * Sanitize a molecular formula string (for _pr_molecular_formula). * - * Strips HTML tags and allows only characters valid in a chemical formula. - * Input is treated as UNTRUSTED. + * Strips HTML tags and control characters, then validates the value against + * an element-formula grammar. Returns the longest leading run of valid + * formula tokens and separators; returns '' when nothing valid is found. + * + * Grammar (per token): [A-Z][a-z]?\d* — an element symbol plus optional count. + * Separators: ( ) [ ] · . and standalone digits (for hydrated formulas). + * Input is treated as UNTRUSTED; downstream JSON escaping is unchanged. + * + * Examples: + * "C62H98N16O22" → "C62H98N16O22" (valid, unchanged) + * "C10H12\ninjection" → "C10H12" (injection stripped) + * "C2H5(OH)" → "C2H5(OH)" (parenthesised formula preserved) + * "" → "" (no valid leading token) + * "" → "" (empty input) * * @param mixed $value Raw input value. - * @return string Sanitized formula string (e.g., "C62H98N16O22"). + * @return string Sanitized formula string, or '' when no valid formula found. */ public static function sanitize_molecular_formula( $value ): string { - $value = sanitize_text_field( wp_strip_all_tags( (string) $value ) ); - return preg_replace( '/[^A-Za-z0-9().\[\]{}]/', '', $value ) ?? ''; + // 1. Strip control characters (including newlines, tabs, null bytes). + $value = preg_replace( '/[\x00-\x1F\x7F]/', '', (string) $value ) ?? ''; + // 2. Strip HTML tags; then strip remaining unsafe text characters. + $value = sanitize_text_field( wp_strip_all_tags( $value ) ); + if ( '' === $value ) { + return ''; + } + // 3. Extract the maximal leading run of valid element tokens and separators. + // A valid run must start with an element token ([A-Z][a-z]?\d*), not a separator. + $pattern = '/^((?:' . self::FORMULA_TOKEN . '|' . self::FORMULA_SEP . ')+)/u'; + if ( preg_match( $pattern, $value, $m ) ) { + return $m[1]; + } + return ''; } /** diff --git a/tests/unit/test-migration-0004.php b/tests/unit/test-migration-0004.php index a6e8776..82562ee 100644 --- a/tests/unit/test-migration-0004.php +++ b/tests/unit/test-migration-0004.php @@ -168,6 +168,12 @@ $single = json_decode( $migration->parse_aliases_string( 'Only One' ), true ); pr_assert_equals( 1, count( $single ), 'single alias produces array of 1' ); // ── sanitize_formula() ─────────────────────────────────────────────────── +// Five required cases (Option B — validate, don't allowlist): +// 1. Valid formula unchanged. +// 2. Parenthesised formula preserved. +// 3. Newline-injected payload — "injection" stripped, formula kept. +// 4. Fully invalid string (script tag contents) → empty. +// 5. Empty / whitespace-only → empty. echo "\nPR_Core_Schema_Sanitizers::sanitize_molecular_formula():\n"; @@ -183,10 +189,22 @@ pr_assert_equals( 'HTML tags stripped from formula' ); +pr_assert_equals( + 'C2H5(OH)', + PR_Core_Schema_Sanitizers::sanitize_molecular_formula( 'C2H5(OH)' ), + 'parenthesised formula preserved unchanged' +); + pr_assert_equals( 'C10H12', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( "C10H12\ninjection" ), - 'non-formula characters removed' + 'non-formula characters removed — newline+word stripped, formula kept' +); + +pr_assert_equals( + '', + PR_Core_Schema_Sanitizers::sanitize_molecular_formula( '' ), + 'fully invalid string returns empty string' ); pr_assert_equals( @@ -195,6 +213,12 @@ pr_assert_equals( 'empty string returns empty string' ); +pr_assert_equals( + '', + PR_Core_Schema_Sanitizers::sanitize_molecular_formula( ' ' ), + 'whitespace-only returns empty string' +); + // ── Idempotency: skips when all three PR Core keys populated ───────────── echo "\nIdempotency (all _pr_* keys populated):\n";