Files
fitme3070.pt/shared/platform-tools/functions_setup.php
T
andreandClaude Opus 5 4d40adbf64 Vendor platform-tools: everything this site runs is now in one repo
shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.

The submodule caused every deployment failure this site has had:

  - .gitmodules named no `branch`, so the update fell back to
    refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
    "Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
    revision in submodule path".
  - Its URL was http://, which the origin answers with 523.
  - This install is deployed by uploading files, and a submodule is invisible
    to that. The tree moved forward while shared/platform-tools stayed on the
    old pin, config/functions_secrets.php died on a missing compat_secrets.php,
    and the whole site — public pages included — went down.

Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.

The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:06:23 +01:00

184 lines
7.8 KiB
PHP

<?php
// ─── First-run setup gate ────────────────────────────────────────────────────
//
// A platform that cannot send mail is a platform where nobody can recover an
// account, no notification arrives, and no contact form reaches anyone -- and
// none of that fails visibly. It fails as silence, weeks later, which is the
// worst way for it to fail.
//
// So SMTP is not optional configuration discovered later: it is part of
// standing the site up. Until it is configured, platform_setup_guard() holds
// the site on the setup page.
//
// Wiring, in the site's bootstrap after the PDO is open and the session
// started:
//
// require_once __DIR__ . '/../shared/platform-tools/functions_setup.php';
// platform_setup_guard($pdo, base_url('setup.php'), [
// 'only' => ['/views/admin/', '/views/sysadmin/'],
// ]);
require_once __DIR__ . '/functions_settings.php';
require_once __DIR__ . '/functions_mail.php';
// Deliberately NOT requiring functions_auth.php. hub2 already defines its own
// auth_login() in config/functions_user.php, so pulling the shared auth in here
// would fatal on a redeclare for any site that has not migrated yet -- and the
// setup gate has to work on exactly those sites. setup.php needs two column
// names, not the whole module.
/**
* The users table and role column, for the setup page's access check.
*
* Uses auth_schema() when the site has wired the shared auth, and the same
* defaults otherwise. Kept separate from auth_schema() so this file has no
* dependency on it: a missing helper must never be the reason a page that
* writes SMTP credentials decides nobody is an admin.
*/
function platform_setup_user_schema(): array {
if (function_exists('auth_schema')) {
$s = auth_schema();
return ['table' => $s['table'], 'role' => $s['role']];
}
return ['table' => 'users', 'role' => 'role'];
}
/**
* Is outbound mail actually configured?
*
* Deliberately checks *resolved* values, not whether a key exists. Every SMTP
* key on three of the four sites existed in .env with an empty value, which a
* presence check reads as configured and a value check reads as broken -- that
* distinction is what hid the estate-wide outage.
*
* A host and a from-address are the minimum: without the first nothing can be
* sent, and without the second nothing that is sent has a sender.
*/
function platform_setup_smtp_ok(PDO $pdo): bool {
$c = platform_smtp_config($pdo);
return trim((string) ($c['host'] ?? '')) !== ''
&& trim((string) ($c['from'] ?? '')) !== '';
}
/** Per-field detail, for rendering the setup form and its warnings. */
function platform_setup_state(PDO $pdo): array {
$c = platform_smtp_config($pdo);
return [
'smtp_ok' => platform_setup_smtp_ok($pdo),
'config' => $c,
'secrets_ok' => platform_settings_secret_available(),
'mailer_present' => function_exists('platform_mail_available'),
'missing' => array_values(array_filter([
trim((string) ($c['host'] ?? '')) === '' ? 'smtp_host' : null,
trim((string) ($c['from'] ?? '')) === '' ? 'email_from' : null,
])),
];
}
/**
* Hold the backoffice on the setup page until SMTP is configured.
*
* `only` lists the path prefixes the gate applies to, and it is the option to
* use: the gate belongs on the backoffice, not the public site. A portfolio, a
* class timetable or a public tool has no reason to go dark because outbound
* mail is unconfigured, and taking the shopfront down to nag the operator
* punishes the wrong person. The backoffice is where the operator already is,
* and where the setup page is reachable from.
*
* With `only` omitted the gate covers everything, minus `exempt`. That is the
* right shape only for an install with no public face at all.
*
* Two exclusions are unconditional. CLI is never gated: a cron worker or a
* migration must still run on a half-configured install, and a redirect means
* nothing without a browser. And a caller that wants JSON gets a 503, never a
* 302 to an HTML page -- anything expecting JSON would parse that as success.
*/
function platform_setup_guard(PDO $pdo, string $setupUrl, array $opts = []): void {
if (PHP_SAPI === 'cli') return;
static $checked = false;
if ($checked) return;
$checked = true;
if (platform_setup_smtp_ok($pdo)) return;
$path = (string) parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
// Never redirect the setup page to itself.
$setupPath = (string) parse_url($setupUrl, PHP_URL_PATH);
if ($setupPath !== '' && str_starts_with($path, $setupPath)) return;
// Scoped mode: anything outside the listed prefixes is public, and stays up.
$only = (array) ($opts['only'] ?? []);
if ($only) {
$inScope = false;
foreach ($only as $prefix) {
if ($prefix !== '' && str_contains($path, (string) $prefix)) { $inScope = true; break; }
}
if (!$inScope) return;
}
foreach ((array) ($opts['exempt'] ?? []) as $prefix) {
if ($prefix !== '' && str_contains($path, (string) $prefix)) return;
}
// A machine caller gets a machine answer. A 302 to an HTML page would be
// parsed as a successful response by anything expecting JSON.
$wantsJson = str_contains((string) ($_SERVER['HTTP_ACCEPT'] ?? ''), 'application/json')
|| str_contains($path, '/api/');
if ($wantsJson) {
http_response_code(503);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'error',
'error' => 'Plataforma por configurar: falta o SMTP.',
]);
exit;
}
header('Location: ' . $setupUrl);
exit;
}
/**
* Save the SMTP settings.
*
* Writes through platform_settings_set(), so smtp_password is encrypted at
* rest by the estate-wide policy rather than by anything decided here. An empty
* password means "leave the stored one alone" -- the field is never rendered
* with the real value, so blank cannot mean "clear it".
*/
function platform_setup_save_smtp(PDO $pdo, array $in, ?int $userId = null): array {
$host = trim((string) ($in['smtp_host'] ?? ''));
$from = trim((string) ($in['email_from'] ?? ''));
$port = (int) ($in['smtp_port'] ?? 587);
$errors = [];
if ($host === '') $errors[] = 'Indique o servidor SMTP.';
if ($from === '' || !filter_var($from, FILTER_VALIDATE_EMAIL))
$errors[] = 'Indique um remetente válido.';
if ($port < 1 || $port > 65535) $errors[] = 'Porta inválida.';
if ($errors) return ['ok' => false, 'errors' => $errors];
platform_settings_set($pdo, 'smtp_host', $host, $userId);
platform_settings_set($pdo, 'smtp_port', (string) $port, $userId);
platform_settings_set($pdo, 'smtp_secure', (string) ($in['smtp_secure'] ?? 'tls'), $userId);
platform_settings_set($pdo, 'smtp_username', trim((string) ($in['smtp_username'] ?? '')), $userId);
platform_settings_set($pdo, 'email_from', $from, $userId);
platform_settings_set($pdo, 'email_from_name', trim((string) ($in['email_from_name'] ?? '')), $userId);
$pw = (string) ($in['smtp_password'] ?? '');
if ($pw !== '') {
// platform_settings_is_secret('smtp_password') is true, so this is
// encrypted on the way in -- no flag passed, no caller decision.
platform_settings_set($pdo, 'smtp_password', $pw, $userId);
}
return ['ok' => true, 'errors' => []];
}
/** Connect-and-authenticate probe against the values just saved. */
function platform_setup_test_smtp(PDO $pdo, string $repoRoot = ''): array {
return platform_mail_test($repoRoot, platform_smtp_config($pdo));
}