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";