Compare commits

...
Author SHA1 Message Date
Martin Ball a5d8165916 Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	ROADMAP.md
2026-11-08 06:19:34 +00:00
Martin Ball 6c290ff67d Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	ROADMAP.md
2026-11-08 06:04:16 +00:00
Martin Ball 917f4e4ca4 Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	ROADMAP.md
2026-11-08 05:50:56 +00:00
Martin Ball 8402940700 Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	ROADMAP.md
2026-11-08 05:37:00 +00:00
Martin Ball 7cccee1f67 Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	ROADMAP.md
;	docs/gaps.md
2026-11-08 05:24:30 +00:00
Dylan M. Taylor 3ded725675 fix(doc): refuse export links holding characters the writers drop 2026-11-03 11:43:30 +00:00
2 changed files with 11 additions and 0 deletions
+1
View File
@@ -257,6 +257,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
- 2026-11-03: Security: DOCX import no longer carries a file's part named like WordCraft's own bookkeeping (`wordcraft:unmodelled.roots` and the like), which a save read back as that bookkeeping, adding relationships the file chose. Readiness unchanged.
- 2026-11-03: Security: LaTeX import parses at most 63 cells of a `tabular` row (what a table row keeps), so a very wide row can no longer widen the multirow tracking that every later row walks. Readiness unchanged.
- 2026-11-03: Security: XLSX and ODS import (mail merge recipient lists) keep each cell's character count as text is added, instead of counting the whole cell again for every piece of text, so a full cell followed by many small text events no longer costs its length each time. Readiness unchanged.
- 2026-11-03: Security: exported hyperlinks (ODT, LaTeX, HTML, Markdown, RTF) holding a control character or noncharacter that the writer drops are written as plain text, so `file\u{85}:///…` can't pass the link check and come out as a `file:` link. Readiness unchanged.
- 2026-11-03: Language packs: language-packs-2 is published and WordCraft pins its index: 75 languages, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch (#4145, #2505) and Swedish (Sweden, Finland), which need craft-proof's flag-based compounding (storytold/craft-libs#497, pinned). English (Australia) is checked against SCOWL's en-AU list as well as en-GB's (storytold/craft-libs#496). Mongolian and Thai stay hidden. Readiness unchanged.
- 2026-11-03: Language lists (CP4.3): a language installed from a Language pack (Bulgarian, say) can now be chosen. Set Proofing Language lists every built-in language and every installed or available pack's, each once and sorted by name; Review › Language and the status bar's menu add the installed packs' languages to the short list, with one way of working out the check mark, by tag. A pack's language is stored as a tag that finds the pack again (`sr-Latn-RS` is proofed by the Latin `sr-latn` pack, not the Cyrillic one; Valencian is Word's `ca-ES-valencia`). Every pack in language-packs-2 now has an English name (Belarusian used to show its own name, Беларуская). Readiness unchanged.
- 2026-11-03: Language packs (CP4.3): on a fresh install (or after an upgrade, with the list cached for an older pin) the missing-pack bar still offers a document language that has no dictionary, "Turkish proofing isn't installed. Install": the person's click turns on downloading, fetches the list and installs the pack if it lists one (if not, the bar goes). With downloading already on, a missing or stale list is fetched once in the background. Choosing a language with only the caret counts before anything is typed, and the spelling context menu's heading (Possible spelling mistake) is in the interface language, not the text's. Closing the bar is remembered per document, per pack and, before the list is known, per language tag. Readiness unchanged.
+10
View File
@@ -19,6 +19,12 @@ pub fn is_web_or_mail_link(url: &str) -> bool {
/// names another machine — following it on Windows leaks the user's NTLM credentials — so it is
/// refused too.
pub fn is_safe_href(url: &str) -> bool {
// Exporters drop other control characters and noncharacters when they write the link
// (`file\u{85}:` comes out `file:`), so a link holding one is refused, not judged as it
// stands and written as something else.
if url.chars().any(|c| (c.is_control() && !c.is_ascii_control()) || matches!(c, '\u{FFFE}' | '\u{FFFF}')) {
return false;
}
let cleaned: String = url.chars().filter(|c| !c.is_ascii_control() && *c != ' ').collect();
// `//host` (protocol-relative) and `\\host` (UNC), with `\` read as `/` the way Windows does.
if cleaned.replace('\\', "/").starts_with("//") {
@@ -200,6 +206,10 @@ mod tests {
"\\\\attacker.example\\share\\x",
"/\\attacker.example\\x",
" //attacker.example/x",
// Characters the exporters drop on writing don't hide a scheme.
"file\u{85}:///etc/passwd",
"java\u{9F}script:alert(1)",
"/\u{FFFE}/attacker.example/x",
"",
] {
assert!(!is_safe_href(bad), "{bad:?}");