*/ private const VALID_KEYS = array( self::KEY_SINGLE_DEFAULT_CARD, self::KEY_SINGLE_COMPACT_CARD, self::KEY_SINGLE_MINIMAL_CTA, self::KEY_SINGLE_FALLBACK_CARD, ); /** * Validated §6.4 template key string. */ private string $value; /** * Private constructor — callers use {@see self::fromString()}. * * @param string $value Pre-validated §6.4 template key string. */ private function __construct( string $value ) { $this->value = $value; } /** * Construct a TemplateKey from a raw string after applying the * §6.4 / §6.5 validation pipeline. * * Validation order: * * 1. Trim and reject empty. * 2. Reject path-traversal bytes (`/`, `\`, `.`, `..`, any * non-ASCII byte) — the most security-critical rejection * runs first per §6.5. * 3. Reject anything not matching `^[a-z][a-z0-9-]{1,63}$`. * 4. Reject anything not in the §6.4 whitelist. * * @param string $key Raw template key string. * * @throws InvalidArgumentException When `$key` violates any of * the rules above. The * message names the offending * input verbatim and the rule * it violated. */ public static function fromString( string $key ): self { $trimmed = trim( $key ); if ( $trimmed === '' ) { $message = 'TemplateKey must be a non-empty string.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( self::containsTraversalByte( $trimmed ) ) { $message = sprintf( 'TemplateKey "%s" violates the path-traversal rule (no "/", "\\", ".", or non-ASCII).', $trimmed ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( preg_match( '/\A[a-z][a-z0-9-]{1,63}\z/', $trimmed ) !== 1 ) { $message = sprintf( 'TemplateKey "%s" violates the §6.4 grammar rule /^[a-z][a-z0-9-]{1,63}$/.', $trimmed ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( ! in_array( $trimmed, self::VALID_KEYS, true ) ) { $message = sprintf( 'TemplateKey "%s" violates the §6.4 whitelist rule (not one of the registered built-in template keys).', $trimmed ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return new self( $trimmed ); } /** * §6.4 whitelist accessor returned in declaration order. * * @return list */ public static function validKeys(): array { return self::VALID_KEYS; } /** * Validated key string. */ public function value(): string { return $this->value; } /** * Kind discriminator for card templates. */ public function kind(): string { return self::KIND_SINGLE; } /** * Whether this key is a single-product card key. */ public function isSingle(): bool { return $this->kind() === self::KIND_SINGLE; } /** * Structural equality check. * * @param self $other Other key to compare against. */ public function equals( self $other ): bool { return $this->value === $other->value; } /** * Test whether the supplied (already-trimmed) string contains * a §6.5 path-traversal byte. * * The rule rejects: * * - `/` and `\` (directory separators) * - any `.` character (covers `.`, `..`, `foo.php`, leading/trailing dots) * - any non-ASCII byte (anything with the high bit set) * * @param string $value Trimmed candidate key. */ private static function containsTraversalByte( string $value ): bool { if ( strpbrk( $value, '/\\.' ) !== false ) { return true; } $length = strlen( $value ); for ( $i = 0; $i < $length; $i++ ) { if ( ord( $value[ $i ] ) > 127 ) { return true; } } return false; } }