Files
fitme3070.pt/config/db.php
T
andreandClaude Sonnet 5 35415429e5 fix(platform-tools): renomeia $user em config/db.php para não colidir com o router
O router (platform.php) requer config/db.php e as páginas partilhadas no
mesmo âmbito plano, sem função a separar — o $user = env('DB_USER')
(a string do utilizador da BD) ficava visível mais tarde em
includes/header.php, que só substitui $user quando está vazio
($user ?? current_user() ?? …). O utilizador de sessão nunca entrava,
e $user['role'] rebentava com TypeError "Cannot access offset of type
string on string" em todas as páginas de sistema (Backups, Auditoria,
Actualizações, IP bans, Origem das visitas) — só a Geo escapava por não
usar esse layout. Confirmado em logs/php_errors.jsonl em produção.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:25:40 +01:00

121 lines
5.6 KiB
PHP

<?php
require_once __DIR__ . '/env.php';
env_load(__DIR__ . '/../.env');
// The legacy CI4 front controller ran from the web root, so some ported view
// partials use root-relative paths (e.g. gallery's glob('uploads/gallery/...')).
// Pin the CWD to the project root so those keep resolving regardless of which
// script Apache invoked.
@chdir(dirname(__DIR__));
// ─── Bootstrap failure helper ────────────────────────────────────────────────
// Renders a generic 500 page. Never echoes exception details — they often
// contain the host:port, DB name, and (worst case) credentials. Real details
// land in logs/db_error.log; the page just says "something went wrong".
function _bootstrap_fail(string $logMsg): void {
@mkdir(__DIR__ . '/../logs', 0775, true);
@error_log('[' . date('c') . '] ' . $logMsg . "\n", 3, __DIR__ . '/../logs/db_error.log');
error_log('[fitme bootstrap] ' . $logMsg);
http_response_code(500);
header('Content-Type: text/html; charset=utf-8');
$tpl = __DIR__ . '/../templates/error_500.php';
if (is_file($tpl)) {
require $tpl;
} else {
echo '<!doctype html><meta charset="utf-8"><title>Erro</title>'
. '<p style="font-family:sans-serif;text-align:center;margin:80px auto;">Algo correu mal. Tente novamente em instantes.</p>';
}
exit(1);
}
// Required env vars — fail loud if any is missing. Hard-coded fallback creds
// (especially `root` / `admin`) were a footgun: a missing .env would let the
// app boot with the wrong identity instead of refusing to start.
foreach (['DB_NAME', 'DB_USER'] as $req) {
if (getenv($req) === false || getenv($req) === '') {
_bootstrap_fail("required env var {$req} is not set");
}
}
if (getenv('DB_PASS') === false) {
_bootstrap_fail('required env var DB_PASS is not set (empty string is allowed)');
}
$host = env('DB_HOST', '127.0.0.1');
$port = (int) env('DB_PORT', 3306);
$db = env('DB_NAME');
// Not $user: that name leaks into the router's flat include scope (router.php
// requires this file at top level, no function wrapper) and would shadow the
// real logged-in user array that includes/header.php expects.
$dbUser = env('DB_USER');
$pass = env('DB_PASS');
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, $dbUser, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// Real server-side prepares (not driver emulation): a tighter defence
// against injection than emulated string-interpolation. Safe here — the
// codebase uses positional ? params (no reused :named, no bound LIMIT).
PDO::ATTR_EMULATE_PREPARES => false,
// Keep values coming back as strings, as the emulated driver did, so the
// type change from native prepares doesn't ripple through existing code.
PDO::ATTR_STRINGIFY_FETCHES => true,
]);
} catch (PDOException $e) {
_bootstrap_fail('PDO connect failed: ' . $e->getMessage());
}
// Distinct session cookie per app so installs that share a host (e.g.
// development.andmore.pt/id.andmore.pt/ and /fitme3070.pt/) don't overwrite
// each other's PHPSESSID. Must run before session_start().
//
// Cookie e modo estrito definidos aqui, no código, para valerem em qualquer
// servidor: antes o cookie saía só com path=/ (sem Secure, HttpOnly nem
// SameSite) e o PHP aceitava IDs de sessão inventados pelo cliente.
if (session_status() === PHP_SESSION_NONE) {
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
if (request_is_https()) ini_set('session.cookie_secure', '1');
session_name('FIT_SESS');
session_start();
}
// ─── Ligação ao platform-tools ───────────────────────────────────────────────
//
// A app mantém as suas próprias passwords e sessão (`config/auth.php`,
// `views/auth/login.php`) — não passa a usar o functions_auth.php partilhado
// (`'auth' => 'own'`), porque a `auth_attempts` desta app não tem a coluna
// `identifier` que o throttle partilhado exige (tem `ip`+`scope`, do seu
// próprio throttle em functions_throttle.php). Ainda assim liga-se ao
// bloqueio de IP da frota, ao router das páginas de sistema (Backups,
// Actualizações, Auditoria, IP bans, Geo, Origem das visitas, Hub) e ao
// resto do platform-tools, tudo por cima da autenticação própria.
require_once dirname(__DIR__) . '/shared/platform-tools/functions_connector.php';
platform_connect($pdo, [
'auth' => 'own',
'guard' => 'require_sysadmin',
'router' => true,
'conformance' => 'report', // nunca recusa — só reporta no health endpoint
// A Comunicações desta app já cobre SMTP (config + testes); o Estado SMTP
// partilhado seria um segundo editor da mesma configuração.
'replaced_tools' => ['smtp_status' => 'views/sysadmin/comms.php'],
'hide_tools' => ['smtp_status'],
'sysadmin_tools' => [[
'title' => 'Comunicações',
'description' => 'Configuração SMTP, fila de mensagens e testes de envio.',
'icon' => 'bi-broadcast',
'url' => 'views/sysadmin/comms.php',
'color' => 'info',
], [
'title' => 'Fila de mensagens',
'description' => 'O que saiu, o que falhou e porquê.',
'icon' => 'bi-envelope-paper',
'url' => 'views/sysadmin/messages.php',
'color' => 'secondary',
]],
]);