Commit Graph

7 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
773670c18f feat(pdf): archival PDF/A output and document properties (#732)
Generated files carried no document properties at all, so an archive of them
showed a column of blank titles and no author. Title, Subject, Author and
Creator are now written from the document number and company, on both drivers:
dompdf via addInfo(), Gotenberg via metadata().

dompdf needed more than the API call. It reads Title from the <title> element
during render(), which happens after addInfo(), so metadata set through the API
alone was silently overwritten by whatever the template put there and the two
drivers disagreed about what the file was called. The title is written into the
markup as well, escaped.

Also adds an archival format setting for Gotenberg: off, PDF/A-1b, -2b or -3b.
PDF/A-3 is what the EU e-invoicing formats expect. Verified against a stock
gotenberg:8 -- LibreOffice inside the image does the conversion and the output
carries the right pdfaid:part in its XMP -- so no extra components are needed.

A fixed list rather than free text, because the SDK forwards whatever it is
given and an unsupported value would surface only as an HTTP error from the
service at render time. Empty is a real choice meaning an ordinary PDF, so it
overrides an env default rather than falling through it.

Gotenberg only: dompdf cannot produce PDF/A.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 14:37:22 +02:00
Darko Gjorgjijoski
713e0bc2e8 feat(pdf): repeating page headers and footers, and page numbers (#729)
Takes over #690 by csoscd. The companion-view idea is theirs; this reworks it
onto the shared page setup and fills in the gaps that stopped it landing.

A `{template}_header` or `{template}_footer` view next to a template is rendered
alongside it and repeated by Chromium on every page. The suffix resolves through
the pdf_templates:: namespace too, so custom templates get it with no extra
wiring.

Two things had to change for that to be useful.

Companion views are now hidden from the template picker. getFormattedTemplates()
lists every .blade.php it finds, so an invoice1_footer would otherwise appear as
a separately selectable template with no preview image -- the feature would have
introduced that the moment anyone used it.

And it does something out of the box. #690 shipped no companion views, so both
of its margin settings were visible no-ops until someone hand-wrote a Blade file.
Instead there is a pdf_page_numbers setting, off by default, that supplies a
footer when a template has none. A template's own companion still wins, so
turning page numbers on cannot overwrite a designed footer.

The setting sits under Gotenberg because only Chromium can repeat a footer;
dompdf has no equivalent. Its value is still carried by the dompdf form so
saving from there cannot clear the choice -- the field is absent from that
payload, and the controller only writes it when present.

Margins come from the page setup rather than #690's separate header_margin and
footer_margin. Chromium draws header and footer inside the page margin, so the
existing margins are the space they occupy; two more settings for the same
distance would have been a second way to say the same thing.

Verified against a live gotenberg:8 on a two-page document: off produces no
footer, on produces "1/2" and "2/2" on the respective pages, and a companion
footer replaces both.

#690's own test is not carried over. It asserted nothing: a bare
View::shouldReceive('exists') is an allowance rather than an expectation,
andReturn(false) never entered the companion branch, and the call sat inside
try { } catch (Throwable) { }, so it passed with the feature deleted.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 12:52:47 +02:00
Darko Gjorgjijoski
a54a5ee007 feat(pdf): one page setup, honoured by both drivers (#728)
Paper size was a Gotenberg-only setting stored as a single "210mm 297mm"
string. dompdf had no page settings at all: size was pinned to config/dompdf.php's
fixed 'a4', its top-level `orientation` key was read by nothing (the installed
barryvdh v3 builds options only from `defines`), and margins were whatever
dompdf's own stylesheet said. So the two drivers disagreed about margins by
default -- dompdf 1.2cm, Gotenberg hardcoded to zero -- and selecting dompdf
silently discarded the paper size.

Replaces gotenberg_papersize with pdf_paper_width / pdf_paper_height /
pdf_orientation / pdf_margin_{top,right,bottom,left}, saved and applied for
either driver. Width and height are separate CSS lengths because that is the
only lossless shared notation: Gotenberg has no named sizes, and dompdf's named
table cannot express everything Gotenberg accepts. Named presets (A3/A4/A5/
Letter/Legal) are a convenience in the UI that resolve to a pair of lengths.

PdfPageSetup resolves it once and translates: a points array plus an orientation
argument for dompdf, CSS lengths plus landscape() for Gotenberg. Both are handed
the portrait pair, since each swaps the axes itself. Gotenberg's margins() takes
top, bottom, left, right, which is not the CSS order.

dompdf exposes no margin API, so DompdfDriver injects an @page rule -- at the
top of <head>, so a template declaring its own still wins. Doing it in the driver
rather than a Blade partial means custom templates get it without including
anything.

Margins default to 1.2cm, dompdf's existing default, so Gotenberg starts
matching it rather than rendering edge-to-edge. Verified against a live
gotenberg:8: A4 portrait, A4 landscape and Letter at zero margins all come out
with the same page box and the same ink offsets on both drivers.

A malformed length now throws rather than being ignored. Blank still falls back,
but a value that is set and wrong is an operator mistake, and the drivers would
otherwise fail differently: dompdf throws converting to points, Gotenberg would
forward the string and render at some other size.

Also here:
- Migration splits an existing gotenberg_papersize into the new pair. It earns
  its place because that key ships in 2.x, not just a 3.x alpha, so a stable
  install that chose Letter would otherwise come back up on A4. Drops
  gotenberg_margins, which 2.x also stores and neither driver ever read.
- Removes EnvironmentManager::savePDFVariables/getPDFConfiguration, which had no
  caller anywhere, and the unused EnvironmentManager injection in the controller.
- config/dompdf.php: drops the dead `orientation` key and defaults enable_remote
  to false, matching .env.example, which sets it explicitly and explains why.
  Installs predating that line were falling back to true.
- Retires the settings.pdf.footer_text and pdf_layout strings, which no component
  referenced.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 12:48:51 +02:00
csoscd
f8cfb6cd33 fix: allow Gotenberg to reach private/Docker-internal hosts (#691)
* fix: allow Gotenberg to reach private/Docker-internal hosts (Issue #688)

The SSRF guard introduced in #664/#671 correctly blocks arbitrary
private URLs, but also prevents legitimate use-cases where Gotenberg
runs alongside InvoiceShelf in a Docker Compose network (e.g. the
default http://pdf:3000 service name resolves to a private IP).

Add a `gotenberg_allow_private_host` setting (env:
GOTENBERG_ALLOW_PRIVATE_HOST, default false) that:
- skips PrivateNetworkGuard in GotenbergPdfDriver
- skips PublicHttpUrl validation in PDFConfigurationRequest
- exposes a clearly-warned toggle in the admin PDF settings UI
- is persisted to the settings table and loaded via AppConfigProvider

A disabled guard is safe for controlled private networks (Docker
Compose, LAN); it must never be enabled for untrusted hosts. The UI
surfaces a prominent warning to communicate this constraint.

Closes #688

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(gotenberg): scope the private-host exemption to a declared host

Reshapes the escape hatch from a boolean admin setting into an
environment-declared host allowlist.

The driver streams the upstream response body back as the PDF, so a
mis-set Gotenberg host is full-response SSRF — pointed at a link-local
metadata endpoint it returns cloud credentials. A blanket "allow
private" switch left that reachable: gotenberg_host stays editable from
the admin UI, so any install that enabled the switch to run a sidecar
could have the host repointed at an internal service. The population the
flag existed to serve was exactly the population it failed to protect.

GOTENBERG_ALLOWED_PRIVATE_HOST now names the single host that may skip
the guard. Only that exact value is exempt; every other private target
stays blocked. GotenbergHostPolicy owns the comparison so the save-time
rule and the runtime driver guard cannot drift, and normalises case,
trailing slash and surrounding whitespace on both sides.

Being env-only also drops the settings-table key, the AppConfigProvider
branch and the whole admin UI surface — the toggle there could not be
switched on in any case, since BaseSwitchSection has no slot and was
passed no v-model, so the child BaseSwitch was discarded and the value
never changed from false.

Restores the gotenberg_margins validation rule, which the previous
revision replaced rather than added alongside.

Tests cover both directions, including that declaring one private host
does not exempt another; sabotaging the policy to always exempt fails 16
of the 22.

Co-authored-by: csoscd <csoscd@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Darko Gjorgjijoski <dg@darkog.com>
Co-authored-by: csoscd <csoscd@users.noreply.github.com>
2026-07-29 10:57:52 +02:00
Darko Gjorgjijoski
99ea898e88 fix(security): block SSRF via the Gotenberg host setting (GHSA-mfxg) (#671)
v3 port. The Gotenberg PDF driver was missed when the SSRF guards were added
to the AI, exchange-rate and file-disk drivers: gotenberg_host was validated
only with 'url', and the driver POSTs the rendered HTML to it.

Reuses the existing infrastructure (consistency with the other drivers):
- Wires App\Rules\PublicHttpUrl into the gotenberg_host validation rule.
- Adds PrivateNetworkGuard::assertAllowed() in GotenbergPdfDriver before the
  outbound call (covers env/seed/stale config + DNS rebinding).

Adds a unit test asserting the gotenberg_host rule rejects private/loopback/
link-local addresses and allows a public one.
2026-06-12 11:02:19 +02:00
Darko Gjorgjijoski
18d63a3375 Configurations cleanup & database configurations for mail and pdfs (#479)
* Move Mail, PDF configuration to Database, standardize configurations

* Set default currency to USD on install

* Pint code
2025-09-19 15:42:53 +02:00
Tim van Osch
bf40f792c2 Feat(Gotenberg): Opt-in alternative pdf generation for modern CSS (#184)
* WIP(gotenberg): add pdf generation abstraction and UI

* feat(pdf): settings validate(clien+server) & save

* fix(gotenberg): Use correct default papersize
chore(gotengberg): Remove unused GOTENBERG_MARGINS env from .env

* style(gotenberg): fix linter/styling issues

* fix(pdf): use pdf config policy

* fix: revert accidental capitalization in mail config vue

* Update composer, remove whitespace typo

* Fix small typos

* fix cookie/env issue

* Add gotenberg to .dev, move admin menu item up
2025-05-04 02:10:15 +02:00