tegory% (%ca is a valid octet) * %date% -> te% (%da is a valid octet) * * while %title%, %author% and %sitename% pass through untouched. The result is * a template that works for most variables and quietly breaks for two of them, * which is far worse than failing outright. * * So template strings are sanitized with the tokens held out of the way. * * @package AiRankSuite */ declare(strict_types=1); namespace AiRankSuite\Support; if ( ! defined( 'ABSPATH' ) ) { exit; } final class Sanitize { /** Matches %token% — letters, digits and underscores only. */ private const TOKEN = '/%([a-z0-9_]+)%/i'; /** * Sanitize a single-line template, preserving %variable% tokens. */ public static function template( string $value ): string { return self::guarded( $value, static fn( string $s ): string => sanitize_text_field( $s ) ); } /** * Sanitize a multi-line template, preserving %variable% tokens. */ public static function template_multiline( string $value ): string { return self::guarded( $value, static fn( string $s ): string => sanitize_textarea_field( $s ) ); } /** * Run $sanitizer with every %token% swapped for an inert placeholder, then * put the tokens back. * * The placeholder uses a NUL-delimited marker: it cannot appear in real * input (sanitization strips control characters, and any that survived * would not match the restore pattern), and it contains no percent sign, * so the sanitizer has nothing to strip. * * @param callable(string): string $sanitizer WordPress sanitizer to apply. */ private static function guarded( string $value, callable $sanitizer ): string { $tokens = array(); $masked = (string) preg_replace_callback( self::TOKEN, static function ( array $m ) use ( &$tokens ): string { $index = count( $tokens ); $tokens[ $index ] = $m[0]; return 'AIRSTOKEN' . $index . 'ENDTOKEN'; }, $value ); $clean = (string) $sanitizer( $masked ); return (string) preg_replace_callback( '/AIRSTOKEN(\d+)ENDTOKEN/', static function ( array $m ) use ( $tokens ): string { // A placeholder with no matching token can only come from input // that already looked like one — leave it as literal text. return $tokens[ (int) $m[1] ] ?? $m[0]; }, $clean ); } }