Andrey Antukh 60d87a6342
🐛 Fix linear gradients in SVG text exports (#11272)
* 🐛 Use gradient type instead of export type in SVG renderer

data->gradient-def was comparing the render `type` parameter (:svg,
:png, :pdf) against "linear" to decide between linearGradient and
radialGradient elements. Since the export type is never "linear",
the comparison always fell through to radialGradient, causing all
linear gradients to be exported as radial in SVG output.

Read the gradient type from the data map instead:
(get-in data ["gradient" "type"])

Closes #5972

* 🐛 Add SVG gradient export regression test

Extract SVG gradient definition generation from the renderer so it can
be tested directly. Add exporter test build wiring and cover both
linear and radial gradient output.

AI-assisted-by: gpt-5.6-luna

* ✨ Standardize exporter testing workflow

Align exporter scripts with the frontend testing pattern. Add a
dedicated GitHub Actions workflow and document the canonical exporter
commands in Serena memories.

AI-assisted-by: gpt-5.6-luna

* ✨ Add focused exporter test execution

Mirror frontend test-runner behavior for focused namespaces and test
vars. Support --focus, --log-level, and --help, and document the
commands.

AI-assisted-by: gpt-5.6-luna

* 🐛 Replace shell exec with execFile in exporter

Replace child_process.exec with execFile to eliminate shell
interpretation. Add hex color validation in exporter and frontend
to reject malformed input before command construction.

This fixes GHSA-4f36-m4hj-cv86 (CVSS 9.9 Critical), an authenticated
OS command injection vulnerability where malicious fill-color values
could execute arbitrary commands in the exporter container.

Defense in depth:
- Layer 1: execFile passes arguments directly without shell parsing
- Layer 2: Exporter validates colors with strict hex regex
- Layer 3: Frontend filters invalid colors before DOM emission

All three independent reporters' attack vectors are addressed:
- Quote breakout (lyhtheori)
- Command substitution (B1gN0Se)
- Path traversal (KimiSecurityTeam)

AI-assisted-by: qwen3.7-plus

* 🐛 Use existing hex-color-string? and fix test path mismatch

Address code review feedback:

- Replace duplicated hex-color-rx and valid-hex-color? with existing
  hex-color-string? from app.common.types.color
- Fix RCE test to use marker path in payload instead of hardcoded /tmp/pwned

AI-assisted-by: qwen3.7-plus

---------

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-19 13:29:04 +02:00

3.1 KiB

Exporter Architecture and Workflow

exporter/: CLJS/Node headless export service. Depends on common/; uses Playwright plus export JS/CLJS deps for SVG/PDF/assets.

Layout and commands

  • Source: exporter/src/; config: deps.edn, shadow-cljs.edn, package.json; runtime helpers/assets: vendor/, scripts/.
  • From exporter/: setup ./scripts/setup; watch pnpm run watch or pnpm run watch:app; production build pnpm run build; test bundle pnpm run build:test; tests pnpm run test or pnpm run test:quiet; lint pnpm run lint:clj; format check/fix pnpm run check-fmt:clj / pnpm run fmt:clj.
  • Because exporter consumes common/, shared file/shape/model changes may need exporter verification even when the immediate change is not under exporter/.
  • Cross-cutting testing principles and anti-patterns: mem:testing.
  • Exporter test conventions and CI: mem:exporter/testing.

HTTP and browser pool

  • POST body limit is about 60 MB. Exporter supports application/transit+json; request params merge query params and body params.
  • Map response bodies are Transit JSON and force HTTP 200; nil 200 bodies become 204.
  • Auth token comes from cookie auth-token, then uploads use Bearer auth plus the management shared key.
  • Each export job gets a fresh Playwright browser context. On success, the context closes and the browser returns to the pool; on error, the browser is destroyed instead of reused.
  • Borrow validates browser connection. Pool acquire timeout is about 10s; font loading timeout logs a warning and continues after about 15s.

Export batching and async behavior

  • prepare-exports groups entries by [scale type] and partitions groups into chunks of 50. Each partition uses file/page/share/name from its first item, so be careful if entries might cross those boundaries.
  • Single-export response is used only when multiple export is not forced and there is exactly one prepared export containing exactly one object.
  • Multi-object export can run async: when wait is false it returns a resource immediately and publishes progress/end/error to Redis by profile topic; when wait is true it waits for upload and returns the uploaded resource.
  • Frame export returns a resource immediately and publishes Redis updates; it does not follow the same wait option path.
  • ZIP entry names are sanitized and duplicates receive numeric suffixes.

Render details

  • Bitmap export differs for WASM vs non-WASM render paths: WASM forces Playwright deviceScaleFactor to 1 and passes scale through the render URL; non-WASM uses deviceScaleFactor = scale.
  • WebP is produced by taking a PNG screenshot and converting it with ImageMagick.
  • SVG export rasterizes text foreignObjects to PNG, converts through PPM/color masks/potrace, and reassembles SVG paths. It also replaces non-breaking spaces for SVG compatibility and drops empty defs/paths.
  • PDF export injects @page sizing through raw browser evaluate JavaScript; that code cannot rely on CLJS runtime helpers.
  • Temporary resources schedule local deletion, then uploads POST to /api/management/methods/upload-tempfile with X-Shared-Key: exporter <management-key> and Bearer auth.