Skip to main content

Content sources

Each document declares a source in config/legal-consent.php:

  • markdown (default) — git-diffable, PR-reviewable .md files under resources/legal.
  • drafts — the admin-maintained draft store. Texts are authored and reviewed per locale in your own screens, and only a reviewed draft set can be released. Opt a document in with 'source' => 'drafts'. See Managing legal texts.

Any other value is your own class implementing Pushery\LegalConsent\Content\LegalDocumentSource, named as the source's driver and resolved from the container. The registry it is named in is sources in config/legal-consent.php, and a document points at an entry by its key:

// config/legal-consent.php
'sources' => [
'markdown' => ['driver' => MarkdownFilesDriver::class, 'path' => null],
'drafts' => ['driver' => DraftDocumentSource::class],

// Yours. The key is what a document's `source` names.
'cms' => ['driver' => \App\Legal\CmsDocumentSource::class],
],

'documents' => [
'terms' => ['source' => 'cms', /* … */],
],

Anything else in the entry is handed to the driver, so a source that needs its own settings takes them there — markdown does exactly that with path.

A custom source is also the operator-identity seam​

A custom source is the seam for interpolating operator identity (Impressum, Art. 13 contact, VAT id, jurisdiction) from a single config source at render time: render your own Blade or HTML with the values filled in and return it as an Html-format body — the pipeline hashes exactly what you rendered, so the acceptance proof still covers precisely what the subject saw. To fill {{ name }} placeholders with every value checked, use Placeholders, below.

Two rules keep it safe:

  • Pin the $locale you are handed. A source that calls __() on the ambient locale hashes the same text differently per viewer and makes legal-consent:check-drift flap.
  • Give every document a real MAJOR.MINOR.PATCH version. A null version defaults to 0.0.0, which can never gate (the gate compares major versions, and held >= 0 is always true), so always set one on a gating document. The exception is a source that counts its own versions, below.

Placeholders, filled and checked​

Pushery\LegalConsent\Content\Placeholders fills {{ name }} placeholders in a text and refuses what it cannot fill. Give it every name a text may use, with its value, and fill the text in your source before you return it:

use Pushery\LegalConsent\Content\ContentFormat;
use Pushery\LegalConsent\Content\Placeholders;

$body = (new Placeholders([
'operator.name' => config('company.name'),
'operator.vat_id' => config('company.vat_id'),
]))->fill($template, ContentFormat::Markdown);

A name the text uses that you did not give, and a name whose value is null or blank, throw UnresolvedPlaceholders. Its message names every one of them, and it lists them in unknown and missing. A value is set on one line and escaped for the format, with backslashes in Markdown and entities in HTML, so it reads as written and never becomes markup.

Rendering a text of your own​

RenderPipeline::render() renders, sanitizes and hashes a text exactly as a published document is rendered and hashed, and publishes nothing: it needs no document key, no version and no acceptance sentence. It is for a text you publish on your own terms, the terms of each shop on a storefront for example, that you still want rendered and hashed the way this package proves a document:

use Pushery\LegalConsent\Content\ContentFormat;
use Pushery\LegalConsent\Content\Placeholders;
use Pushery\LegalConsent\Content\RenderPipeline;

$rendered = app(RenderPipeline::class)->render(
$shop->terms_markdown,
ContentFormat::Markdown,
new Placeholders(['shop.name' => $shop->name, 'shop.domain' => $shop->domain]),
);

$rendered->html; // the sanitized HTML
$rendered->contentHash; // the hash a published document of the same text carries
$rendered->sourceHash; // the hash over the text, its placeholders filled

The placeholders are filled first, so both hashes cover the text as it is shown. A text over the pipeline's byte limit throws LegalDocumentTooLarge, before its placeholders are filled and after.

A source that counts its own versions​

Some texts come from a system that numbers their revisions itself, often with a whole number, and keeps the archive of which revision a customer read. Return that number as sourceVersion on the RawDocument:

return new RawDocument(
type: $type,
locale: $locale,
title: $text->title,
body: $text->html,
format: ContentFormat::Html,
uiWording: $text->wording,
sourceVersion: (string) $text->revision, // what the other system calls this text
);

It is recorded word for word, beside the SemVer version, on the published version (source_version, PublishedDocument::$sourceVersion, PublishedVersion::$sourceVersion) and on every consent given to it (document_source_version, also in each entry of history()). It decides nothing: the gate reads the SemVer version alone.

  • Without a SemVer version, each new source version is the next minor of the active version, and the first one is 1.0.0. A minor raises nobody's consent, so publish these with a mode that does not gate, such as SilentEditorial. A change that has to ask everybody again is a major you state as version, and it goes through the same guards as any major.
  • The same source version publishes as the version it was. Publishing an unchanged text again changes nothing, and a changed text under a source version that was published already is refused: a source version names one text.
  • One to 64 printable ASCII characters, with no space at either end. Anything else is refused with InvalidSourceVersion rather than trimmed, because the source version is folded into the record hash of every consent given to the text.