CluPilotCloud/app/Services/Legal/DpaRenderer.php

110 lines
4.3 KiB
PHP

<?php
namespace App\Services\Legal;
use App\Console\Commands\PruneDormantAccounts;
use App\Console\Commands\PruneUnverifiedAccounts;
use App\Support\CompanyProfile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\View;
use TCPDF;
/**
* The processing agreement and its annex, as PDFs.
*
* TCPDF, because the invoices already render through it — a second PDF engine
* for two documents a year would be a dependency nobody maintains. Its HTML
* support is not a browser's, so the two Blade views are deliberately plain:
* headings, paragraphs, lists, one table.
*
* ## The documents are generated, not uploaded
*
* They name the company from CompanyProfile and take their deadlines from the
* commands that enforce them, so the agreement cannot drift from the invoices or
* from what the software actually does. An operator can still upload a lawyer's
* revision as a new version — the console does not care where a file came from.
*/
final class DpaRenderer
{
/**
* The sub-processors this installation actually talks to.
*
* Here rather than in the template because it is a fact about the system,
* not wording: hosting, payment and mail are the three places customer-
* related data leaves this application, and a list that quietly omits one is
* the single most common defect in a processing agreement.
*
* @return array<int, array<string, string>>
*/
public static function subprocessors(): array
{
return [
[
'name' => 'Hetzner Online GmbH',
'service' => 'Rechenzentrum, Server und Netzanbindung der Kundeninstanzen',
'location' => 'Falkenstein und Nürnberg, Deutschland; Helsinki, Finnland',
],
[
'name' => 'Stripe Payments Europe, Ltd.',
'service' => 'Zahlungsabwicklung (Vertrags- und Zahlungsdaten des Kunden, keine Instanzinhalte)',
'location' => 'Irland (EU)',
],
[
'name' => 'thinkidoo e.U.',
'service' => 'Mailserver für den Versand und Empfang der Betriebs- und Support-E-Mails',
'location' => 'Österreich',
],
];
}
/** The agreement itself. */
public function agreement(string $version, ?Carbon $issuedOn = null): string
{
return $this->render('legal.dpa.agreement', $version, $issuedOn, [
'subprocessors' => self::subprocessors(),
]);
}
/** Annex 1: the technical and organisational measures. */
public function measures(string $version, ?Carbon $issuedOn = null): string
{
return $this->render('legal.dpa.measures', $version, $issuedOn, [
// From the commands that enforce them, so the document cannot
// promise one deadline while the sweep runs another.
'unverifiedDays' => PruneUnverifiedAccounts::AFTER_DAYS,
'warnDays' => PruneDormantAccounts::WARN_DAYS_BEFORE,
]);
}
/** @param array<string, mixed> $data */
private function render(string $view, string $version, ?Carbon $issuedOn, array $data): string
{
$issuedOn ??= now();
$html = View::make($view, array_merge($data, [
'version' => $version,
// R19: written on the wall clock, because a date on a contract is
// read by a person and not by a server.
'issuedOn' => $issuedOn->local()->isoFormat('LL'),
]))->render();
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8');
$pdf->SetCreator('CluPilot');
$pdf->SetAuthor((string) (CompanyProfile::get('name') ?? 'CluPilot'));
$pdf->SetTitle(__('dpa.title').' '.$version);
// No default header or footer bar: this is a contract, not a report, and
// TCPDF's default header prints a black line and a title nobody set.
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(true);
$pdf->SetMargins(20, 18, 20);
$pdf->SetAutoPageBreak(true, 20);
// DejaVu, because the text is German: TCPDF's core fonts have no ß in
// the encoding this uses and would print it as a blank.
$pdf->SetFont('dejavusans', '', 9.5);
$pdf->AddPage();
$pdf->writeHTML($html, true, false, true, false, '');
return (string) $pdf->Output('', 'S');
}
}