diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7049284..9ae0271 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,51 @@ jobs: - name: Check out repository uses: actions/checkout@v7 + - name: Lint WordPress plugin PHP + run: find wordpress-plugins/calorieapp-identity-bridge -type f -name '*.php' -print0 | xargs -0 -n1 php -l + + - name: Test legal footer compatibility + shell: bash + run: | + php <<'PHP' + Chamber of Commerce KVK: 84216352

' + . '

Calorie Token • KvK 84216352

' + . '

ICTHendrikse • KvK 73774693

' + . '

Operator: ICTHendrikse

' + . '

© 2023 Calorie Token

' + . '

© 2026 CalorieToken

' + . '

Calorie aims to be the World's food token

' + . '

Unrelated content remains unchanged.

'; + $output = LegalFooterCompatibility::replace_legacy_footer_html($input); + $operator = 'Operator: ICTHendrikse · KVK 73774693'; + $copyright = '© 2026 ICTHendrikse (owned content only) · CalorieToken® trade mark: Pieter Hendrikse'; + + if (substr_count($output, $operator) !== 4) { + throw new RuntimeException('Known operator footer variants were not normalized.'); + } + if (substr_count($output, $copyright) !== 2) { + throw new RuntimeException('Known copyright footer variants were not normalized.'); + } + if (!str_contains($output, 'Calorie aims to be the world’s food token')) { + throw new RuntimeException('The legacy footer tagline was not normalized.'); + } + if (!str_contains($output, 'Unrelated content remains unchanged.')) { + throw new RuntimeException('Unrelated rendered content changed.'); + } + + $current = ''; + if (LegalFooterCompatibility::replace_legacy_footer_html($current) !== $current) { + throw new RuntimeException('The current footer is not idempotent.'); + } + PHP + - name: Set up Python uses: actions/setup-python@v7 with: diff --git a/wordpress-plugins/calorieapp-identity-bridge/README.md b/wordpress-plugins/calorieapp-identity-bridge/README.md index c3c1c5d..4967313 100644 --- a/wordpress-plugins/calorieapp-identity-bridge/README.md +++ b/wordpress-plugins/calorieapp-identity-bridge/README.md @@ -25,7 +25,7 @@ The browser endpoint is intentionally not REST. XUMM Login establishes a normal Details are in SECURITY.md and CONFIGURATION.md. -Version 0.1.3 adds a narrowly scoped public-HTML compatibility layer that replaces obsolete legal footer labels left in cached Brizy output. It does not modify stored page, product, Tokenomics, payment, or authentication data. Version 0.1.2 adds a normal WordPress browser authorization handler for XUMM-authenticated sessions while retaining the REST exchange contract. Version 0.1.1 aligned state validation with the backend's signed v1 protocol +Version 0.1.4 completes the narrowly scoped public-HTML footer compatibility layer by normalizing the known legacy Brizy footer variants to the current operator, KVK, copyright, trademark, and tagline text. It does not modify stored page, product, Tokenomics, payment, or authentication data. Version 0.1.3 introduced the compatibility layer. Version 0.1.2 adds a normal WordPress browser authorization handler for XUMM-authenticated sessions while retaining the REST exchange contract. Version 0.1.1 aligned state validation with the backend's signed v1 protocol (timestamp, nonce, and HMAC-SHA256 signature) and supports an exact allowlisted external callback without requiring a separate WordPress redirect-host filter. diff --git a/wordpress-plugins/calorieapp-identity-bridge/TESTING.md b/wordpress-plugins/calorieapp-identity-bridge/TESTING.md index 49b4d97..bae285b 100644 --- a/wordpress-plugins/calorieapp-identity-bridge/TESTING.md +++ b/wordpress-plugins/calorieapp-identity-bridge/TESTING.md @@ -28,8 +28,9 @@ Covered scenarios: 20. backend state validation uses timestamp/nonce/HMAC headers 21. backend state validation does not transmit the shared secret 22. canonical WordPress REST root is documented for `/index.php/wp-json/` deployments -23. obsolete cached legal footer labels are replaced in rendered HTML -24. unrelated rendered HTML remains unchanged +23. known obsolete cached legal footer variants are normalized in rendered HTML +24. the current footer remains unchanged when the filter runs repeatedly +25. unrelated rendered HTML remains unchanged ## Files diff --git a/wordpress-plugins/calorieapp-identity-bridge/calorieapp-identity-bridge.php b/wordpress-plugins/calorieapp-identity-bridge/calorieapp-identity-bridge.php index 960dd7b..64fae77 100644 --- a/wordpress-plugins/calorieapp-identity-bridge/calorieapp-identity-bridge.php +++ b/wordpress-plugins/calorieapp-identity-bridge/calorieapp-identity-bridge.php @@ -3,7 +3,7 @@ * Plugin Name: CalorieApp Identity Bridge * Plugin URI: https://calorietoken.net * Description: Companion bridge plugin that maps authenticated WordPress/XUMM sessions to short-lived CalorieApp authorization codes. - * Version: 0.1.3 + * Version: 0.1.4 * Author: CalorieApp * License: GPL-2.0-or-later * License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/wordpress-plugins/calorieapp-identity-bridge/includes/class-calorieapp-identity-bridge-legal-footer-compatibility.php b/wordpress-plugins/calorieapp-identity-bridge/includes/class-calorieapp-identity-bridge-legal-footer-compatibility.php index cd76a67..7c0b0db 100644 --- a/wordpress-plugins/calorieapp-identity-bridge/includes/class-calorieapp-identity-bridge-legal-footer-compatibility.php +++ b/wordpress-plugins/calorieapp-identity-bridge/includes/class-calorieapp-identity-bridge-legal-footer-compatibility.php @@ -15,10 +15,22 @@ * been rebuilt. */ class LegalFooterCompatibility { - private const LEGACY_OPERATOR_PATTERN = '/Chamber of Commerce KVK:\\s*[0-9]{8}/'; - private const CURRENT_OPERATOR = 'Operator: ICTHendrikse'; - private const LEGACY_COPYRIGHT = '© 2023 Calorie Token'; + private const FLEXIBLE_SPACE = '(?:\\s| | |\\x{00A0})*'; + private const LEGACY_OPERATOR_PATTERNS = [ + '/Chamber' . self::FLEXIBLE_SPACE . 'of' . self::FLEXIBLE_SPACE . 'Commerce' . self::FLEXIBLE_SPACE . 'KVK' . self::FLEXIBLE_SPACE . ':' . self::FLEXIBLE_SPACE . '[0-9]{8}/iu', + '/Calorie' . self::FLEXIBLE_SPACE . 'Token' . self::FLEXIBLE_SPACE . '(?:•|·|•|·)' . self::FLEXIBLE_SPACE . 'KvK' . self::FLEXIBLE_SPACE . ':?' . self::FLEXIBLE_SPACE . '[0-9]{8}/iu', + '/Operator:' . self::FLEXIBLE_SPACE . 'ICTHendrikse' . self::FLEXIBLE_SPACE . '(?:•|·|•|·)' . self::FLEXIBLE_SPACE . 'KvK' . self::FLEXIBLE_SPACE . ':?' . self::FLEXIBLE_SPACE . '73774693/iu', + '/(?Chamber of Commerce KVK: 12345678

' + public function test_replaces_known_obsolete_legal_footer_variants(): void { + $html = '

Chamber of Commerce KVK: 84216352

' + . '

Calorie Token • KvK 84216352

' + . '

ICTHendrikse • KvK 73774693

' + . '

Operator: ICTHendrikse

' . '

© 2023 Calorie Token

' + . '

© 2026 CalorieToken

' + . '

Calorie aims to be the World's food token

' . '

Tokenomics content remains unchanged.

'; $result = LegalFooterCompatibility::replace_legacy_footer_html($html); - $this->assertStringContainsString('Operator: ICTHendrikse', $result); + $this->assertSame(4, substr_count($result, 'Operator: ICTHendrikse · KVK 73774693')); + $this->assertSame( + 2, + substr_count( + $result, + '© 2026 ICTHendrikse (owned content only) · CalorieToken® trade mark: Pieter Hendrikse' + ) + ); $this->assertStringContainsString( - '© 2026 ICTHendrikse (owned content only) · CalorieToken® trade mark: Pieter Hendrikse', + 'Calorie aims to be the world’s food token', $result ); $this->assertStringContainsString('Tokenomics content remains unchanged.', $result); - $this->assertStringNotContainsString('12345678', $result); + $this->assertStringNotContainsString('84216352', $result); + } + + public function test_current_footer_is_idempotent(): void { + $html = ''; + + $this->assertSame($html, LegalFooterCompatibility::replace_legacy_footer_html($html)); } public function test_leaves_unrelated_html_unchanged(): void {