Repository source: docs/plans/php_ascii_restrictions_current_state_v1.md
PHP ASCII Restrictions Current State V1
This document describes how the v3 application currently handles ASCII restrictions in April 2026. It is intended as a factual brief for discussion with an advisor, not as a proposal for what the policy should become.
Since superseded in part: a FORUM_UNICODE_AUTHORED_TEXT feature flag
now exists (FeatureFlagRegistry::UNICODE_AUTHORED_TEXT, default off),
which answers this doc's own "which record families should move to UTF-8
first?" question — post subject/body — via
LocalWriteService::normalizeAuthoredLine()/UnicodeTextPolicy. A
companion FORUM_EMOJI_AUTHORED_TEXT flag layers emoji support on top when
Unicode text is enabled. The rest of this brief (ASCII tokens, identifiers,
usernames, canonical repository storage of everything else) is still
accurate as of this writing; only the "does Unicode ever survive into
canonical storage" answer has changed, and only when the flag is on.
Executive Summary
The current system is built around an ASCII-only canonical write contract:
canonical posts are stored as ASCII text files
canonical identity bootstrap records are stored as ASCII text files
canonical public-key files are stored as ASCII-armored text files
several identifiers are restricted further to ASCII token subsets
In practice, this means the backend remains the final enforcement point. The browser adds a limited compose-time compatibility layer for a few common punctuation characters, but it does not change the underlying storage rule.
The result is a hybrid system:
repository state is intentionally ASCII-only
the browser tries to smooth over a narrow class of accidental non-ASCII input
unsupported non-ASCII content is still blocked from submission
Core Policy Boundary
The key architectural decision is that canonical repository artifacts are plain ASCII files.
This is stated directly in the current specs:
docs/specs/canonical_post_record_v1.md: one post file is one ASCII text recorddocs/specs/identity_bootstrap_record_v1.md: one identity bootstrap file is one ASCII text recorddocs/specs/public_key_storage_v1.md: one public-key file stores one ASCII-armored OpenPGP key
So the current question is not whether the system stores Unicode canonically. It does not. The only question is how much normalization and recovery the application provides before backend enforcement rejects non-ASCII content.
Where ASCII Is Enforced Today
The main enforcement logic lives in src/ForumRewrite/Write/LocalWriteService.php.
There are three different restriction levels in that file.
1. Printable ASCII line fields
normalizeAsciiLine() applies to single-line human text fields such as thread subjects:
trims leading and trailing whitespace
allows only bytes in the printable ASCII range
0x20through0x7Erejects everything else with
"<field> must be printable ASCII."
Current use:
subjectincreateThread()
Important consequence:
tabs, newlines, smart punctuation, accented letters, Cyrillic, emoji, and all other non-ASCII characters are rejected
2. ASCII token fields
requireAsciiToken() applies to structured identifiers and route-like values:
trims whitespace
requires a non-empty token
only allows
A-Z a-z 0-9 . _ : -rejects anything else with
"<field> is required and must be an ASCII token."
Current use includes:
thread_idparent_idbootstrap_post_idtarget_profile_slugauthor_identity_idindirectly through identity validation
Important consequence:
these fields are more restricted than general printable ASCII
spaces are not allowed
punctuation is heavily constrained
3. ASCII body fields
normalizeAsciiBody() applies to multiline text bodies and armored public keys:
normalizes CRLF to LF
trims outer whitespace
requires a non-empty value
allows LF plus printable ASCII bytes
rejects other bytes with
"<field> must be ASCII only."appends one trailing LF before storage
Current use includes:
post
bodyreply
bodypublic_key
Important consequence:
ordinary multiline text is allowed, but only in ASCII
non-ASCII letters and symbols are rejected even when the field is conceptually “free text”
Field-By-Field Current State
Thread creation
In createThread():
board_tagsare normalized bynormalizeBoardTags()subjectmust passnormalizeAsciiLine()bodymust passnormalizeAsciiBody()author_identity_idmust pass retained OpenPGP identity rules
board_tags are a special case. They are not rejected as raw ASCII/non-ASCII in the same way as subject and body. Instead they are normalized aggressively:
lowercased
stripped to
a-z,0-9, spaces, and hyphenswhitespace collapsed
defaulted to
generalif emptied out
This means board_tags are not merely ASCII-only. They are reduced to a small canonical subset.
Reply creation
In createReply():
thread_idandparent_idmust be ASCII tokensbodymust be ASCII-only multiline textboard_tagsgo through the same restrictive normalization as thread creation
Replies do not have a subject field in the current implementation.
Identity linking
In linkIdentity():
public_keymust passnormalizeAsciiBody()the fingerprint is normalized to uppercase ASCII hex
the stored identity ID uses lowercase ASCII hex
the username is normalized separately, not accepted raw
The username normalization lives in src/ForumRewrite/Security/OpenPgpKeyInspector.php:
lowercases the source user ID
converts runs of non
[a-z0-9._-]characters to-trims leading and trailing
-falls back to
guest
So username handling is not “preserve ASCII if possible.” It is canonicalization into a narrow ASCII slug.
Approval and identity-reference flows
Approval-related fields such as approver_identity_id, target_identity_id, thread_id, parent_id, and target_profile_slug are all constrained to retained ASCII token forms.
In addition, requireOpenPgpIdentityId() further narrows identity IDs:
must start with
openpgp:fingerprint must be lowercase hex only
referenced identity file must exist
Browser Behavior Today
The browser-side compose logic lives in public/assets/browser_signing.js.
It does not make the system Unicode-capable. It only provides a narrow compatibility layer ahead of backend enforcement.
Safe automatic replacements
normalizeComposeAscii() automatically rewrites a small fixed set of common characters:
left single quote
‘to'right single quote
’to'left double quote
“to"right double quote
”to"en dash
–to-em dash
—to-ellipsis
…to...non-breaking space to ordinary space
This is intentionally narrow. It is punctuation cleanup, not general transliteration.
Unsupported character detection
After that replacement pass, any remaining non-ASCII character is treated as unsupported by compose validation.
Current compose behavior:
unsupported characters remain visible in the field while the user types
the field gets an inline error
the form gets a compose-level error
submit is blocked until unsupported characters are removed
each affected field exposes an explicit
Remove unsupported charactersaction
This is now a preservation-and-recovery flow, not a silent deletion flow.
Draft preservation
The compose script also stores drafts in localStorage for user-editable fields. This is a resilience feature, not an encoding-policy feature, but it affects how ASCII restrictions are experienced:
unsupported text can remain in the draft
reload does not inherently destroy that text
the user can recover and edit before cleanup
Important Distinction: Browser Policy Versus Backend Policy
The browser currently implements a friendlier policy than the backend, but only at the edges.
Browser policy:
automatically normalize a tiny set of punctuation
preserve other unsupported characters in the form
explain the problem inline
require explicit cleanup before submit
Backend policy:
reject non-ASCII content for canonical text fields
reject non-token characters for token fields
store only ASCII canonical artifacts
This means the browser is a UX layer around the ASCII rule. It is not the source of truth.
If browser JavaScript fails to load or is bypassed, the backend still enforces the ASCII restrictions directly.
What “ASCII-Only” Means In Practice Here
The phrase “ASCII-only” is doing multiple jobs in the current system.
Storage format
Canonical records are stored as plain text files that are meant to be:
easy to inspect
easy to diff in git
simple to parse deterministically
ASCII is part of that simplification strategy.
Canonicalization strategy
The system prefers normalized, low-variance textual forms:
lowercase slugs for usernames
lowercase or uppercase hex fingerprints depending on field
restricted token alphabets for identifiers
LF-normalized bodies
So ASCII is not just an encoding preference. It is tied to the broader desire for canonical low-ambiguity text.
Product constraint
For end users, ASCII-only means:
ordinary English punctuation often works after browser cleanup
many non-English writing systems cannot be posted directly
unsupported text can be drafted, but not submitted as-is
That is a significant product limitation, not just a storage detail.
Places Where The Policy Is Uneven
The current implementation is not one perfectly uniform ASCII policy. It is a collection of related restrictions.
Examples:
subjectuses printable ASCII validationbodyuses multiline ASCII validationboard_tagsuse a narrower normalization-and-strip ruleidentifiers use ASCII token validation
usernames are slugified from user IDs rather than accepted literally
compose pages get browser-side punctuation cleanup, but the backend itself does not normalize those characters
This matters for advisor discussion because “the app is ASCII-only” is true, but incomplete. The real system is a layered set of different ASCII-derived contracts.
Current Benefits Of The Restriction
From an engineering standpoint, the current approach buys:
simple plain-text canonical records
deterministic parsing rules
stable git diffs
fewer encoding edge cases in repository-backed state
simple identifier normalization rules
These benefits are real and currently embedded in the implementation.
Current Costs Of The Restriction
From a product and UX standpoint, the current approach costs:
inability to canonically store non-English text
friction when mobile keyboards emit smart punctuation
compose-time recovery logic that would be unnecessary in a Unicode-native system
policy leakage from storage format into user experience
need for special-case normalization and cleanup affordances
These costs are also real and visible in the current compose flow.
Questions This Document Does Not Answer
This document does not recommend whether the system should remain ASCII-only. It only describes current behavior.
Questions for advisor discussion include:
Should canonical posts remain ASCII-only?
If yes, is the current browser normalization/recovery layer sufficient?
If no, which record families should move to UTF-8 first?
Are identifiers and usernames appropriately constrained today, or overly normalized?
Is the current “simple git-friendly text” goal worth the product limitation on multilingual content?
Bottom Line
Today, v3 treats ASCII as a canonical storage rule, not just an implementation detail.
The browser softens that rule for a narrow set of punctuation and provides recovery UX for unsupported characters, but the backend still rejects non-ASCII canonical content. In other words:
Unicode may appear temporarily in compose state
Unicode does not survive into canonical post or identity storage
token-like fields are restricted even more tightly than general ASCII text
That is the current state of the system.