24730 Commits

Author SHA1 Message Date
alonso.torres
e71da3b419 ✨ Add lang to text shapes and ranges in the Plugin API
The paragraph language selects the CJK typesetting rules (line breaking,
punctuation spacing, the default emphasis position and localized glyphs),
but plugins could not read or set it. `lang` holds it as a BCP 47 tag, as
the HTML `lang` attribute does; `null` or `''` removes it.

On a text shape it applies to every paragraph and reports 'mixed' when
they differ. On a text range it applies to the whole paragraphs the range
touches: `update-text-range-attrs` now writes paragraph attrs such as the
language there instead of copying them into spans. The tag pattern moves
to `schema:text-language`, shared by the content schema and the plugin
validator.

AI-assisted-by: claude-opus-5-5
2026-11-12 00:48:37 +01:00
alonso.torres
842ac55193 🐛 Set digits tate-chu-yoko upright and centred in vertical text
Four fixes to how `digits` tate-chu-yoko composites lay out in vertical
text:

- A lone digit combines too, as CSS `digits <n>` combines runs of n or
  fewer, so it stands upright instead of turning sideways. The renderer
  (`split_digit_runs`) and the export path (`digit-combine-segments`)
  share the rule.
- Composites centre on the OS/2 typographic metrics, like upright glyphs.
  The hhea ascent of CJK faces exceeds the em, which set digits about
  0.07em below the kanji beside them.
- Letter-spacing stays outside the composite, as in CSS. Counting the
  trailing spacing in its width moved the digits left by half of it.
- The regional Chinese profiles no longer add the Han/Western gap around
  a composite: it sets in one ideograph's cell, as JLREQ already does.

AI-assisted-by: claude-opus-5-5
2026-11-12 00:48:37 +01:00
alonso.torres
6ec98398e1 🐛 Join text spans left with the same style after range styling
Styling a range splits spans at its ends, and nothing joined them back, so
a text kept invisible boundaries between spans with equal styles. Two CJK
features depend on those boundaries: tate-chu-yoko `all` combines per
span, so "2" + "027" rendered as two upright blocks, and a ruby reading or
warichu note over a range that crossed a hidden boundary was rejected.

`merge-equal-spans` joins neighbouring spans of a paragraph whose styles
match, counting a missing attr as its default value. Spans with a reading
or a warichu note are never joined: each one is its own group. Both places
that split spans use it: `update-text-range-attrs` (plugin API) and
`apply-attrs-to-paragraph` (WASM editor styling).

AI-assisted-by: claude-opus-5-5
2026-11-12 00:48:37 +01:00
alonso.torres
376f3b096a ✨ Add distributed text alignment and preserve fractional line spacing
Add distributed alignment to text styles, sidebar controls, CSS export,
and the Plugin API. Spread characters across horizontal and vertical
lines, including the final line, while preserving graphemes and annotations.

Keep fractional line heights consistent across painting, hit testing,
selection, and text paths to prevent baseline drift between lines.

Add tests for distribution, nonbreaking text, and fractional line geometry.

AI-assisted-by: gpt-6
2026-11-12 00:48:37 +01:00
alonso.torres
e40e615b80 🐛 Restore selected text when composition is cancelled
Restore the pre-composition content and UTF-16 selection when an IME ends
with an empty replacement. Keep styles, selection direction and redo intact
without recording cancellation as an edit. Share snapshot restoration with
editor undo and redo.

Add a regression for styled selections and cancellation after undo. Verify
commit, cancellation, deletion and history in four saved showcase examples,
including emoji and decomposed Korean Jamo in both writing directions.

AI-assisted-by: GPT-6
2026-11-12 00:48:37 +01:00
alonso.torres
05446cf76e 📎 Cover native Hanja conversion in editor history
Exercise the Hangul-to-Hanja composition sequence through the public editor
bindings. Keep previews out of saved content and history, then restore the
styled source selection with one undo and the converted content with redo.
Cover both writing directions and Korean wrapping modes.

Verify native Fcitx Hangul conversion in all four showcase examples and
confirm the saved examples survive reopening.

AI-assisted-by: GPT-6
2026-11-12 00:48:28 +01:00
alonso.torres
d79775251b 🐛 Restore undo and redo inside the WASM text editor
Keep bounded content and selection history within each editing session.
Group committed compositions and styled insertions, omit IME previews,
and reset history when external content changes the editing baseline.

Route keyboard, workspace menu and browser history input to the editor.
Restore UTF-16 selections through a new WASM binding, preserving styles,
readings and paragraph settings without reaching earlier canvas edits.

Verify 1,587 frontend tests, 671 renderer tests and both WASM builds.
Add four editable showcase examples and verify them after reopening.

AI-assisted-by: GPT-6
2026-11-12 00:48:28 +01:00
alonso.torres
43a8c683a2 🐛 Keep text editing open when an IME cancels composition
Remember when a key-down belongs to native composition. Consume its
Escape release after composition ends, preserving the active text editor.
Keep ordinary Escape behavior for leaving editing and closing menus.

Add regression coverage for native key ordering and composition markers.
Verify 1,583 frontend tests, lint and changed-file formatting. Check native
Pinyin, Zhuyin and Korean input with eight editable showcase examples.

AI-assisted-by: GPT-6
2026-11-12 00:48:20 +01:00
alonso.torres
edc241b767 🐛 Preserve emphasis across adjacent ruby spans
Recover merged annotation styles on the base character's own line, using
separate UTF-16 line anchors and UTF-8 style keys. Include emphasis marks
in horizontal text bounds so standalone exports keep each mark whole.

Add regression coverage for adjacent readings, wrapped text, emoji line
anchors, raster output and export bounds across all text grow modes.
Verify 670 renderer tests, formatting and both WASM builds. Add four
editable showcase examples and check their layout and exports.

AI-assisted-by: GPT-6
2026-11-12 00:48:20 +01:00
alonso.torres
471803a446 🐛 Keep indivisible CJK units together in narrow text boxes
Retain legal line starts at the original wrap width before allowing an
indivisible punctuation or annotation unit to overflow together. Share the
layout plan and alignment offset across painting, editing and exports.

Cover mixed styles, nonbreaking controls, graphemes, annotations, alignment,
strokes and resize. All 666 renderer tests and both WASM builds pass.

AI-assisted-by: GPT-6
2026-11-12 00:48:20 +01:00
alonso.torres
9cca7066ee ✨ Add Mandarin Zhuyin reading layout
Add typed one-character Zhuyin readings with upright right-side symbols
and separate neutral and non-neutral tones in both writing modes.
Persist the mode through the editor, WASM bridge and Plugin API. Preserve
annotation clearance and canvas, PNG, PDF and SVG geometry.

Validate syntax and selection boundaries, and cover schema, bridge,
rendering, UI and API behavior. Native IME and reader review remain open.

AI-assisted-by: gpt-6
2026-11-12 00:48:10 +01:00
alonso.torres
9f0082c2a4 ✨ Add Chinese emphasis and underline positions
Carry optional span positions through the schema, WASM header, editor,
Plugin API and CSS output. Expose Chinese controls with language defaults
and independent overrides for horizontal and vertical text.

Reserve automatic clearance per side and stack underlines, ruby and
emphasis. Preserve reading groups during decoration edits and retain
canonical position values in DOM editor style data.

Add protocol, geometry, editing and API tests, including the prior Japanese
mixed-font gutter behavior. Verify both WASM targets and live exports.

AI-assisted-by: GPT-6
2026-11-12 00:48:10 +01:00
alonso.torres
4dfbc503b2 ✨ Add Chinese Pinyin reading controls
Support typed character and group readings through the shared ruby model.
Expose Chinese labels, presentation controls and selection guards without
changing Japanese controls or generating pronunciation.

Preserve complete source ranges for glyphs sharing a combining cluster.
Add pinned horizontal and vertical tone-mark fixtures and editor tests.

AI-assisted-by: GPT-6
2026-11-12 00:48:10 +01:00
alonso.torres
0fd183fccb ✨ Add CJK layout validation across box sizes
Cover Chinese regional profiles and Korean wrap modes across fitting
widths, style boundaries, punctuation, numeric units and decomposed Jamo.
Check stable offsets and finite geometry in narrower horizontal boxes,
and keep indivisible punctuation units together in short vertical boxes.

AI-assisted-by: GPT-6
2026-11-12 00:48:10 +01:00
alonso.torres
c66859714d ✨ Add Chinese regional and Korean word layout
Add named Chinese punctuation presets and a Korean keep-words mode for
horizontal and vertical layout. Preserve the saved spaces-only mode and
keep source offsets stable through safe emergency wrapping.

Apply bounded CJK spacing and justification, expose the new choices in the
sidebar, and document preset bounds and remaining release checks.

Verify renderer, frontend, common and exporter tests, both WASM builds,
and sample save/reload plus PNG, PDF and SVG exports. Existing CI failures
remain documented in the layout notes.

AI-assisted-by: GPT-6
2026-11-12 00:48:10 +01:00
alonso.torres
ee8b08439d ✨ Add language-specific CJK layout controls
Add independent Japanese, Chinese and Korean file switches and all-files
preferences under the shared vertical-text-layout feature flag. Keep
control availability separate from saved paragraph profiles and rendering.

Expose explicit language profiles in separate sidebar sections. Preserve
mixed values, report the active paragraph profile at the caret, and keep
shared writing-mode controls consistent without radio group collisions.

Validation: common JVM/JS and frontend suites, backend profile tests,
exporter tests and lint pass. Existing runner formatting and Spanish
translation QA issues remain unchanged.

AI-assisted-by: GPT-6
2026-11-12 00:48:09 +01:00
alonso.torres
8dbdb00fde ✨ Add a CJK text layout proof of concept
Choose paragraph layout rules from an optional language tag. Pass language
to horizontal and vertical shaping, add basic Chinese punctuation rules,
and add experimental Korean spaces-only wrapping.

Keep text boundaries grapheme-safe and preserve paragraph profiles across
editor splits and merges. Add reproducible font fixtures for localized
glyphs and regression tests for the shared WASM protocol and layout paths.

Validation: renderer, common JVM/JS, frontend and exporter tests pass.
Both WASM targets build. Full CI still reports baseline lint and format
errors and one backend multipart-upload test failure.

AI-assisted-by: GPT-6
2026-11-12 00:47:52 +01:00
alonso.torres
f8dd0fc2d9 🐛 Fix windows IME support 2026-11-12 00:47:42 +01:00
alonso.torres
747c63920f ✨ Change iconography to reflect orientation 2026-11-12 00:47:42 +01:00
alonso.torres
1e254cf644 🐛 Fix issues with auto-width/height 2026-11-12 00:47:42 +01:00
alonso.torres
47e348cdbc ✨ Expose Japanese text layout in the Plugin API
Expose Japanese layout properties on Text and TextRange with validated
values and whole-shape writing mode behavior. Keep ruby readings tied
to their spans when applying styles through ranges.

Update public TypeScript declarations, the API changelog and runtime
and plugin test coverage, including active-page checks.
2026-11-12 00:47:42 +01:00
alonso.torres
67ff2f67ac ✨ Add Japanese layout controls and opt-in settings
Enable Japanese layout per file through undoable changes, or for all
files through a saved profile preference. Show the controls when the
WASM renderer and either setting are active.

Add writing mode, orientation, ruby, emphasis, warichu and line fitting
controls with icons and English and Japanese translations. Cover the
file change, profile preference and control behavior.
2026-11-12 00:47:42 +01:00
alonso.torres
bea9a40c31 ✨ Edit Japanese text with vertical cursors and IME input
Apply Japanese attributes to text selections and whole shapes without
splitting ruby or warichu spans. Respect vertical writing when resizing
text and placing selection handles and pointer cursors.

Anchor native IME input to the WASM caret, keep composition previews
out of the store until commit, and add an optional geometry overlay.
Cover editor styles, serialization, fallback fonts and cursor behavior.
2026-11-12 00:47:28 +01:00
alonso.torres
221ac11f22 ✨ Preserve Japanese text in browser rendering and export
Render Japanese annotations in HTML and SVG and expose their styles
in CSS code generation. Preserve vertical position data from WASM,
while keeping the legacy SVG renderer on its horizontal layout path.

Cover generated markup, styles and copied SVG output.
2026-11-12 00:47:09 +01:00
alonso.torres
991a93aa98 ✨ Render Japanese text layouts in WASM
Lay out horizontal and vertical Japanese text with kinsoku, ruby,
emphasis, warichu, tate-chu-yoko and proportional font features.
Carry the layout through glyph paths, SVG export and text selection.

Update the Rust and ClojureScript binary formats together, including
the editor bridge and headless exporter. Resolve CJK fallback fonts
and add renderer tests and font fixtures.
2026-11-12 00:47:09 +01:00
alonso.torres
b43d7cf81e ✨ Define Japanese text layout attributes
Add schemas, defaults and helpers for vertical writing, ruby, emphasis
and warichu. Keep readings and notes tied to their own characters when
copying styles, and preserve base text when pasting HTML ruby.

Cover the model, property copying, clipboard and library file builder.
2026-11-12 00:46:57 +01:00
penpot-renovate[bot]
b438676d6c
⬆️ Update dhi.io/node image to f6ba5e2 (#12975)
- dhi.io/node 24.22.2-debian13-dev@f6ba5e2 (docker/images/Dockerfile.media-processor)
- dhi.io/node 24.22.2-debian13-dev@f6ba5e2 (docker/images/Dockerfile.mcp)
- dhi.io/node 24.22.2-debian13-dev@f6ba5e2 (docker/images/Dockerfile.exporter)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-11 16:55:19 +01:00
Gabriel Ribeiro
7e9a3c3c28 📚 Fix typos in technical and user guides
Fix five misspelled words in the docs: "notifiy" and "occured" in the
integration guide, "enviroment" in the developer guide index, "succesful"
in the authentication subsystem page and "achive" in the layers page.

AI-assisted-by: claude-opus-5-5
Signed-off-by: Gabriel Ribeiro <267068126+gabrielribeiro3105@users.noreply.github.com>
2026-11-11 15:24:13 +01:00
andrés gonzález
a301c2b43f
✨ Keep board names visibility after reloading the workspace (#12872)
Hiding board names from the View menu reset on every reload, while
rulers and comments kept their state. Add the flag to the layout flags
persisted in local storage so it behaves the same way.

Closes #12871

AI-assisted-by: claude-opus-5-5
2026-11-11 14:52:35 +01:00
Andrey Antukh
671ff4a05f Merge remote-tracking branch 'origin/staging' into develop 2026-11-10 20:06:46 +01:00
Andrey Antukh
8172059b2e Merge branch 'staging-2.19.x' into staging 2026-11-10 19:53:23 +01:00
Andrey Antukh
5e5722dfe7 📚 Update changelog 2.19.1 2026-11-10 19:24:39 +01:00
Andrey Antukh
a58ace4df4 Merge branch 'staging-2.19.x' 2026-11-10 19:18:41 +01:00
Andrey Antukh
d5c4bfb234 ✨ Add security fixes section to the changelog flow
The 🔒 section is generated from advisories instead of being written
by hand: an advisory becomes an entry when its patched_versions include
the release version, so unpublished (triage/draft) advisories are valid.

- gh.py advisories --patched-version filters on the exact token of
  vulnerabilities[*].patched_versions; the list output keeps the summary
  schema.
- changelog.py security renders the entries sorted by GHSA id, links the
  merged milestone PRs labelled "pr pt" that reference the GHSA id and
  warns when a reference has no matching patched version. The subsection
  follows 🚀 (or 💥, or the version header) and is removed
  when the version has no advisories.
- Tests cover the filter, the entry rendering, PR discovery and the
  subsection placement.
- Memories and the update-changelog skill document the contract.
- CHANGES.md: add 2.19.1 with its security fixes and the remaining bug
  fix.

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 18:18:00 +00:00
Andrey Antukh
e4fcc663c0 Merge remote-tracking branch 'origin/staging-2.19.x' into staging 2026-11-10 18:09:07 +01:00
Andrey Antukh
88b09efd87 📚 Note breaking changes must not be hard-wrapped
The changelog generator copies the note lines verbatim, so a note
hard-wrapped at 80 columns ends up wrapped in CHANGES.md. Record
that the breaking-change note must keep one line per paragraph or
bullet, in both the issue and changelog workflow memories.

AI-assisted-by: inkling-2-small-free
2026-11-10 16:58:21 +00:00
Yamila Moreno
a3f50887b3 📚 Improve 2.19 documentation regarding admin-console
Signed-off-by: Yamila Moreno <yamila.moreno@kaleidos.net>
2026-11-10 16:13:06 +01:00
Andrey Antukh
1db1487c7f
🐛 Require thread access when marking threads as read (#12894)
* 🐛 Require thread access when marking threads as read

mark-all-threads-as-read took any list of thread ids and wrote the read
status of the caller on each of them, without checking that the caller
can access the file that owns the thread. It was the only comment
mutation that did not check permissions, and it took the ids as an
unbounded array, so a single request could keep a database connection
and a transaction open for a long time.

The rpc now resolves the threads in one query and checks the comment
permissions of every distinct file, answering not-found when a thread
does not exist or is not accessible. Both cases answer the same error
on purpose, so the response cannot be used to tell existing threads
apart.

The status rows are now written with a single insert, keeping the
modified-at date one second in the future that makes the thread count
as read. Repeated ids are deduped, the command returns nil again like
the sibling commands with no result, and the schema limits the number
of threads per call to 500 while the dashboard sends the ids in batches
of that size.

Tests cover access rejection, unknown threads, mixed batches that write
nothing, repeated ids, empty lists, marking twice, and the frontend
batch split.

Relates to GHSA-j4vc-cmf6-jhr7

AI-assisted-by: minimax-m3.1-flash

* 🐛 Refresh the unread list when a batch fails

The dashboard sends the thread ids in batches of 500 and each batch
commits on the server. A failure in a later batch left the unread list
stale: the re-read only ran when every batch succeeded, so the threads
already marked as read stayed visible as unread.

The re-read now runs in both paths. On failure the event is emitted
before the error is propagated, so the error toast still shows and the
success toast stays limited to a full success.

The backend also answers an empty list without opening a transaction,
since there is nothing to check and nothing to write.

Relates to GHSA-j4vc-cmf6-jhr7

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 15:54:11 +01:00
Andrey Antukh
8510daef52
🐛 Restrict the team invitation token and list to admins (#12892)
* 🐛 Restrict the team invitation token and list to admins

The token of a team invitation is a bearer credential: whoever holds it
accepts the invitation with the role of the invitation row. The RPC that
mints it only required edition permissions, while the rest of the
invitation commands require admin, so a team editor could get the token of
any pending invitation, including the ones that grant the owner role.

The token now requires admin, and the token of an owner invitation is
restricted to the team owner: an admin cannot create nor promote an owner
invitation, so it cannot keep its token either. It reuses the error code
already used for that rule.

Minting the token now leaves an audit event named as an action, carrying
the granted role and the id of the invitation, so the extraction does not
go unnoticed.

The list of pending invitations is admin only as well: it carries the
email of the invited person and the role offered, and the dashboard was
painting those emails to any member. The frontend catches the permission
error and shows the permission notice instead, and the copy action is not
offered for an owner invitation, which the backend rejects for an admin.

The five invitation commands now answer with the same permission error,
so they speak one language.

Relates to GHSA-4mmc-j8rh-74pf

AI-assisted-by: minimax-m3.1-flash

* 🐛 Clear the cached invitations and gate the list on team admin

The pending invitations are a team admin only list. The dashboard still
offered the entry to every member and keyed the section on the invite
policy of the organization, so a team admin blocked by that policy saw
the no-permission notice although the backend lets them list.

- `fetch-invitations` answers a denied refetch with an
  `invitations-fetched` event carrying an empty list, so a demoted admin
  does not keep the cached emails and the header no longer paints the
  invite button from a stale list.
- The header tabs and the team options dropdown hide the invitations
  entry without `:is-admin`, which already includes the team owner.
- `invitations-section-content` keys the section on `:is-admin`, and the
  invite policy only gates the invite actions: resending keeps the
  `can-invite?` check, deleting and row selection only need team admin.

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 15:53:43 +01:00
Andrey Antukh
0420db1ca9
🐛 Apply the share-link page scope to the comment queries and writes (#12888)
* 🐛 Apply the share-link page scope to the comment queries and writes

A share link can be restricted to a subset of the pages of a file, and
the comment queries enforced that scope. But the RPC that resolves the
profiles of the commenters authorized the request with
check-comment-permissions! and dropped the returned perms, so its query
filtered only by file id. The holder of a page scoped link got the id,
email, fullname and photo of every profile that commented on any page
of the file, including the pages the owner left out of the link.

The query now takes the pages of the share link and only selects the
authors of the threads on them. The profile of the caller is still
added, so the viewer keeps its own avatar.

The scope was missing as well on the comment writes that take a page id
or a thread id: create-comment-thread accepted a page outside the link,
create-comment accepted a thread of an unshared page, and
update-comment-thread, update-comment-thread-position and
update-comment-thread-frame accepted a thread of an unshared page. They
now raise not-found, the same way the comment queries already did.
Replying to a thread of an unshared page also sent the emails to the
thread participants.

The page scope rule now lives in one place: share-link-pages returns
the pages granted by a share link and check-page-scope raises not-found
when a page is outside of it, so get-comment-threads reuses the helper
instead of checking the pages inline.

The docstring of get-profiles-for-file-comments said that it returns
the participants of all the comment threads of the file, which is not
true anymore for a share link. The docs are public, so the text and
the changes list are updated.

The tests of the share link page scope are hardened: the setup asserts
that the comment threads are created, the membership case has a second
commenter so it can no longer pass for the wrong reason, and the link
without pages, the allowed paths of the thread updates and the
who-comment=team denial are covered.

Relates to GHSA-89cq-3wv7-987r

AI-assisted-by: minimax-m3.1-flash

* 🐛 Enforce the share-link page scope on the remaining comment writes

Check the page scope of the share link before mutating in the four
comment writes that still skipped it: update-comment-thread-status,
update-comment, delete-comment-thread and delete-comment. The check
runs before the ownership checks, so an out-of-scope write fails with
not-found and the response does not reveal that the comment or thread
exists. Load the file in create-comment only after the permissions and
page scope checks, matching create-comment-thread, so a rejected reply
does not load the file data.

Cover the denial and the in-scope paths of the four commands, and the
fail-closed case of a link without pages on the comment reads. Move
the outsider thread creation after the profiles assertion so the
caller-union assertion fails when the union is removed. Document the
page scope invariant in the backend permissions memory.

AI-assisted-by: deepseek-v4.1-flash
2026-11-10 15:53:07 +01:00
Andrey Antukh
4874da450f
🐛 Escape SVG values and sanitize every SVG upload (#12887)
* 🐛 Escape SVG values and sanitize every SVG upload

The SVG sanitizer removed dangerous elements and attributes by name and
then wrote the tree back with clojure.xml/emit, which escapes nothing.
A payload hidden in an attribute value or in a text node became live
markup again: an id of "x' onload='alert(1)'" was stored as a real
onload handler, and an entity encoded script tag was stored as a real
script element.

Write the filtered tree with a serializer that escapes text and
attribute values, and route every path that stores an uploaded SVG
through it. The thumbnail writers (create-file-object-thumbnail and
create-file-thumbnail) and the organization logo stored the upload
untouched, so they could plant live markup in a public bucket.

The filter only knows about SVG elements, so it also accepted
documents that are not SVG at all: a page with an iframe or a meta
refresh was stored as an image. The sanitizer now rejects any document
whose root is not an svg element in the SVG namespace, and it compares
the scheme of an url the way the browser does, so a scheme split with
a tab or a line break no longer slips through the javascript: filter.
The attributes are written sorted by name, because the parser hands
them over in the reverse order of the document and the output changed
on every sanitization.

The profile picture of an identity provider is not stored either when
it is an SVG: the avatar is always rendered as an image, so an SVG
would only add the risk of keeping live markup in the storage. The
dimensions of an SVG are now read from its local name, so an SVG with
a namespace prefix can be uploaded like any other.

The logo had no size limit and the sanitizer reads the whole file, so
it now honors the media max file size.

The output shape changes with it: no line breaks, double quotes, and
the single quote of an attribute value escaped as an entity. The hash
of every stored SVG object changes with it.

The media tests use cuerdas.core instead of clojure.string, as the rest
of the codebase does.

Relates to GHSA-qwhj-rchj-58mg

AI-assisted-by: minimax-m3.1-flash

* 🐛 Drop non-SVG namespace subtrees in the SVG sanitizer

The sanitizer only checked the namespace of the root, so a child could
rebind xmlns to XHTML and smuggle live HTML (iframe/srcdoc/meta) past the
blocklist. The serializer escapes attribute values, but the browser decodes
them again, so srcdoc stays live.

Track the namespace bindings through the recursion and drop any element
that is not in the SVG namespace, with its subtree. The root is validated
up front with the same model, so a document that is not an svg document is
rejected with an honest hint instead of a misleading one.

The default namespace is stored under a namespaced key, because a prefix
literally named `default` collided with the internal key and re-opened the
same hole through a prefixed binding. The attribute filter now keeps
`xmlns`/`xmlns:*` declarations, which are not event handlers, so a prefix
named like one does not end up undeclared in the output.

The blocklist also drops HTML-only elements written in the svg namespace
(iframe, meta, embed, link, base).

Add regression tests for the child rebinding, for the prefixed variants,
for the `default` prefix, for the root local name, for the
create-file-media-object-from-url path (mocked download), and for the
HTML-only tags. Assert double-quoted attributes in the clean content test.

Relates to GHSA-qwhj-rchj-58mg

AI-assisted-by: longcat-2.5-free
2026-11-10 15:52:28 +01:00
Andrey Antukh
c39e3e8301
✨ Strictly validate uploads against file bytes on disk (#12801)
* ✨ Strictly validate uploads against file bytes on disk

Sniff the real content type of every upload with simplemagic magic
bytes instead of trusting the client-supplied mtype header. Declared
types outside the allowlist keep the not-allowed code; allowed
declarations whose bytes differ are rejected as mismatch. Covers SVG
by probing its root tag over leading bytes (magic never matches XML
text), TTF/OTF by magic name and WOFF/WOFF2 by four-cc.

Read the real file size from disk instead of trusting the declared
:size. Files over the limit are rejected even when the declared size
lies downwards, and small files pass even when it lies upwards.

Run the path check first everywhere so forged server paths keep the
params-validation code. Recheck from-url downloads after the write,
with a declared-only fast fail at header time. Thread the assembled
path through font validation.

Refresh the affected tests around real and sparse fixture files,
including a PNG-as-font chunked-upload rejection case.

Closes #12796

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Accept BOM-prefixed SVG uploads and cover WOFF/WOFF2/OTF/WEBP

Address review comments on #12801:

- SVG files saved with a UTF-8 BOM were rejected as
  :media-type-mismatch: read-head kept U+FEFF and the <svg probe
  never ran. A leading BOM is now stripped before the probe, and a
  generic XML sniff (application/xml, text/xml) is treated as
  "no magic evidence" for declared image/svg+xml, so the root-tag
  probe decides whether the file opens with an <svg root.
- sniff-match?/svg-file? docstrings claimed the magic database never
  matches SVG; simplemagic 1.17 does match an SVG with an XML prolog
  as image/svg+xml. Reworded to match reality.
- Added matching-pass tests for WOFF, WOFF2, OTF and WEBP (they hit
  the read-four-cc, magic-name and direct magic branches) and three
  BOM SVG regression cases.

AI-assisted-by: inkling-2-small-free
2026-11-10 15:51:48 +01:00
Elena Torró
7f0b62b994
🐛 Keep drop shadows out from under translucent shapes (#12904)
* 🐛 Keep drop shadows out from under translucent shapes at every zoom

Cut the shape out of its drop shadow, as CSS box-shadow does, and stop
large shadows from being cut off at high zoom.

AI-assisted-by: claude-opus-5-5

* 🐛 Keep drop shadows out from under translucent shapes in SVG and PDF

Match the canvas: clip the shape out of its drop shadow on export.

AI-assisted-by: claude-opus-5-5
2026-11-10 15:05:08 +01:00
Juanfran
e1f924cf00 🐛 Send organization invitations to unregistered emails (#12937)
send-template drops nil values before enqueuing the mail job, but the
invite-to-organization schema required the :user-name key. Inviting an
email with no Penpot account left :user-name nil, the key was dropped
and the job failed its schema check, so the invitation email was never
sent. :user-name is now optional in invite-to-organization and in
renewal-notice, which has the same shape.

New test renders both templates through the queue with a nil
:user-name.

AI-assisted-by: claude-opus-5-5
2026-11-10 14:12:27 +01:00
penpot-renovate[bot]
c9d3cbb383
⬆️ update GitHub Actions (#12898)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-renovate.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker-devenv.yml)
- docker/login-action v4.8.0 (.github/workflows/build-docker-devenv.yml)
- zizmor 1.32.0 (.github/workflows/lint-workflows.yml)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
Co-authored-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-11-10 13:48:10 +01:00
Miguel de Benito Delgado
cbd4c9634c
🔧 Use correct :cache-root in shadow-cljs.edn (#12883) 2026-11-10 13:47:50 +01:00
Marina López
4e8f05171e
🐛 Forward attribute deletion events to admin console (#12932) 2026-11-10 13:47:13 +01:00
Alonso Torres
79d4936cae
⬆️ Release plugins and mcp (#12934)
* ⬆️ Updated plugins release to 1.6.0

* ⬆️ Updated mcp release to 2.19.1
2026-11-10 13:44:51 +01:00
Juanfran
ae24118caf
🐛 Send organization invitations to unregistered emails (#12937)
send-template drops nil values before enqueuing the mail job, but the
invite-to-organization schema required the :user-name key. Inviting an
email with no Penpot account left :user-name nil, the key was dropped
and the job failed its schema check, so the invitation email was never
sent. :user-name is now optional in invite-to-organization and in
renewal-notice, which has the same shape.

New test renders both templates through the queue with a nil
:user-name.

AI-assisted-by: claude-opus-5-5
2026-11-10 13:44:26 +01:00
Andrés Moya
10b08d5224 🐛 Improve sync of layout item attributes in components (#12851)
* 🐛 Preserve layout item touched groups when switching variant

* 🔧 Add systematic tests to check layout items component sync

* 🔧 Add small updates after comments

* 🔧 Add tests for partial overrides
2026-11-10 13:41:54 +01:00