Skip to content

feat: profile pictures for spaces and maintained resources, with SVG support (#632, #634) - #636

Merged
tkuhn merged 4 commits into
masterfrom
feat/632-634-profile-pictures
Aug 24, 2026
Merged

feat: profile pictures for spaces and maintained resources, with SVG support (#632, #634)#636
tkuhn merged 4 commits into
masterfrom
feat/632-634-profile-pictures

Conversation

@tkuhn

@tkuhn tkuhn commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #632, closes #634.

Profile pictures for spaces and maintained resources (#632)

A space or maintained resource that declares schema:image on its own IRI shows that picture above its title and main URI, plainly — no tilted-square mask like user icons get. With no picture declared, the header renders exactly as before.

Only declarations signed by a current admin of the governing space count. The new query RALK8_WQ…/get-resource-profile-picture follows the shape of get-view-displays-unresolved: it runs on repo/full and makes one federated hop to repo/spaces for the authority gate (npa:hasGoverningSpaceRef → admin RoleInstantiationnpa:AccountState), reflexive for a space itself and pointing at the maintainer space for a maintained resource. Candidates are found subject-first, so that hop only ever sees a handful of rows. The maintainer tier is deliberately not accepted, and the gate is not pinned to a single ref — for an identifier with rival claimants, an admin of any claiming ref can declare a picture.

The picture is fetched inside the resource's existing asynchronous data update, next to the view displays, so rendering never waits on it and a post-publish forced refresh picks up a new picture with the rest of the structure.

Per the issue, there is no About-page option to set one yet: a picture is declared by publishing the triple.

SVG markup as a profile picture (#634)

A schema:image value may be a literal holding the SVG itself rather than a link — for users as well as for spaces and maintained resources.

ProfilePicture is the one place that interprets the value: SVG markup goes through Utils.sanitizeSvg, gets an xmlns injected when the author left it out (an <img> parses SVG as XML and fails without one), and travels on as a data: URI. Every picture site stays a plain <img>, so the mask on user icons, object-fit on resource pictures and list-row sizing keep working for either kind of value — and inside an <img> an SVG document can neither script nor fetch, so a picture cannot reach into the page showing it.

Setting one through the UI needs a template whose field accepts a literal. The live one is URL-only and cannot be superseded from another account (latest-version resolution joins on the same pubkey), so this is published instead:

  • template RAz5tNdG…/template — one statement with an nt:ValuePlaceholder ("image URL, or SVG markup in quotes"), space-governed by https://w3id.org/spaces/knowledgepixels/nanodash so its members can edit it collaboratively
  • kind registration RA6owO2C…<kind> a gen:MaintainedResource ; gen:isMaintainedBy <…/nanodash>
  • profile view RATPxXJT… — the "update profile image..." action points at the template's embedded IRI

No code change is needed for governed float: the action URL carries template-version=latest, and TemplateData.getLatestTemplateId routes governed pins through get-latest-governed-version.

Two bugs found on the way

Long literals overflowed the stack. isValidLiteralSerialization / getParsedLiteral used patterns shaped ^"(([^\\"]|\\\\|\\")*)"… — an alternation inside a greedy star, which Java's regex engine evaluates with one stack frame per character. A ~15 KB SVG pasted into a value field threw StackOverflowError in the publish form's validator, i.e. a 500. The quoted part is now scanned linearly.

getEscapedLiteralString never escaped quotes. replaceAll("\"", "\\\\\"") passes \" as a replacement string, where the backslash is an escape, so it emitted a bare quote and any value containing one round-tripped into an unparseable serialization. Now uses String.replace. UtilsTest.getEscapedLiteralStringEscapesQuotesCorrectly had codified that bug (it asserted the input came back unchanged, while its backslash sibling asserted real escaping), so its expectation is corrected here.

SVG exported by drawing tools rendered all black. Serif/Affinity-style exports put every paint in style="fill:rgb(…)", and the sanitizer allows no style attribute — so the declarations were dropped and every shape fell back to the SVG default fill. Safe declarations are now rewritten into the equivalent presentation attributes before sanitizing, so the existing allow-list validates them like any other attribute: the property must be one of the paint/text ones, and the value may contain no function call other than rgb/rgba/hsl/hsla(…), so url(…) and expression(…) cannot survive. This also fixes inline SVG in cells, lists and paragraphs, not just profile pictures.

Verification

Driven against a local instance on a separate port (dev server untouched, nothing published from it):

  • space and maintained-resource pages with no picture (header unchanged), with a linked image, and with a wide logo
  • an admin-signed declaration on test/group renders; the authority join was validated against live data with a stand-in predicate before publishing, a bogus-role variant returns 0 rows, and the two non-space subjects in the network that do carry schema:image are correctly rejected
  • an SVG literal renders in the user-page header and the session icon, mask intact
  • the new template loads through its embedded IRI, a quoted SVG reaches the signed preview as a literal, a URL as an IRI, and a 28 KB literal no longer 500s

23 new unit tests (ProfilePictureTest, LiteralSerializationTest, SvgStyleSanitizationTest); full suite green at 1176.

🤖 Generated with Claude Code

tkuhn and others added 4 commits August 24, 2026 17:10
A space or maintained resource that declares schema:image on its own IRI now shows
that picture above its title and main URI, plainly -- no tilted-square mask like user
icons get, and no fallback: a resource without a declared picture renders exactly as
before.

Only declarations signed by a current admin of the governing space count, checked by
the new get-resource-profile-picture query: the resource's npa:hasGoverningSpaceRef
edge (reflexive for a space itself, the maintainer space for a maintained resource)
is joined to the admin RoleInstantiations of that ref and on to npa:AccountState, so
a space's picture is not something an unrelated agent can set -- unlike a user's
self-declared one, which keeps the subject-agnostic get-all-user-profile-pics.

The picture is fetched inside the resource's existing asynchronous data update, next
to the view displays, so rendering never waits on it and a post-publish forced refresh
picks up a new picture with the rest of the structure.

No About-page option to set one yet (per the issue): a picture is declared by
publishing the triple.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A schema:image value may now be a literal holding the SVG itself, instead of a link to
an image file -- for users as well as for spaces and maintained resources.

The new ProfilePicture is the one place that interprets the value: SVG markup is
reduced to the same static subset as an SVG view's output (Utils.sanitizeSvg), gets an
xmlns injected when the author left it out (an <img> parses SVG as XML and fails
without one), and travels on as a data: URI. Every place that shows a picture stays a
plain <img>, so the tilted-square mask on user icons, the object-fit rules on resource
pictures and the sizing in list rows keep working for either kind of value -- and
inside an <img> an SVG document can neither script nor fetch, so a picture can never
reach into the page showing it.

UserData now skips values that are usable as neither, rather than parsing them as
IRIs: a bare Values.iri there would abort the whole user-data load on the first SVG
literal it met.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The patterns behind isValidLiteralSerialization and getParsedLiteral had the shape
^"(([^\\"]|\\\\|\\")*)"..., an alternation inside a greedy star, which Java's regex
engine evaluates with one stack frame per character. A value of a few thousand
characters -- an SVG picture pasted into a value field, say -- therefore threw
StackOverflowError inside the publish form's validator and turned the request into a
500. The quoted part is now scanned linearly; only the @lang / ^^<iri> suffix is still
matched by a regex.

getEscapedLiteralString never escaped quotes either: replaceAll("\"", "\\\\\"") passes
\" as a replacement string, where the backslash is an escape, so it emitted a bare
quote and any value containing one round-tripped into an unparseable serialization. It
now uses String.replace, which takes its arguments literally.

UtilsTest.getEscapedLiteralStringEscapesQuotesCorrectly had codified that bug -- it
asserted the input came back unchanged, while its backslash sibling asserted real
escaping -- so its expectation is corrected here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drawing tools export SVG with the paint in a style attribute
(style="fill:rgb(120,184,134);") rather than in presentation attributes. The sanitizer
allows no style attribute -- it is the one attribute whose value can pull in external
resources -- so those declarations were dropped and every shape fell back to the SVG
default fill: an all-black figure wherever such a picture or figure was shown.

The safe declarations are now rewritten into the equivalent presentation attributes
before sanitizing, so the existing allow-list validates them like any other attribute.
Nothing new is allowed through: the property must be one of the paint/text ones, and
the value may contain no function call other than rgb/rgba/hsl/hsla(...), so url(...)
and expression(...) cannot survive. stroke-miterlimit, clip-rule and stroke-dashoffset
join the attribute list, as tool exports commonly set them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tkuhn
tkuhn merged commit 8e49949 into master Aug 24, 2026
8 checks passed
@tkuhn
tkuhn deleted the feat/632-634-profile-pictures branch August 24, 2026 18:21
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 5.12.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow for direct SVG in profile picture triples Profile pictures for Spaces

1 participant