feat(audit): platform-tools atualizado (12f26fd) e registo de auditoria ligado
- shared/platform-tools: ea3616a → 12f26fd (27 commits). Traz o módulo de audit (f761d99, o pin da frota), «manter sessão iniciada», o registo de segurança da frota (o gancho que o login já tinha passa a funcionar) e a correção do IP do cliente encontrada nesta auditoria (último X-Forwarded-For / X-Real-IP), promovida ao partilhado. client_ip() da app delega nele. - config/functions_audit.php: audit() sobre platform_audit(); tabela audit_log criada no primeiro uso, segredos ocultados, nunca parte a ação. - Registado: login, falha de login, bloqueio por excesso de tentativas, logout, pedidos de recuperação, mudança/definição de palavra-passe, registo e criação de utilizadores, convites, perfil (antes/depois), todas as definições (db_update_setting, com valor anterior), cada passo da lixeira (apagar, restaurar, eliminar de vez) e ações nas mensagens. - Sistema → Auditoria (views/sysadmin/audit.php), só sysadmin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e300fc341
commit
0e8b53f620
65 files changed
+5852
-455
No files matched your search
@@ -15,6 +15,11 @@
|
||||
// backup_trigger(string $type): bool ($type in {files, db})
|
||||
// backup_type_label(string): string
|
||||
// backup_type_badge(string): string
|
||||
// backup_retention_policy(): array
|
||||
// backup_retention_summary(?array $policy = null): string
|
||||
// backup_created_at(string $filename, int $fallback): int
|
||||
// backup_gfs_keep(array $items, array $policy, int $now): array
|
||||
// backup_prune(bool $dryRun = false): array
|
||||
|
||||
function backup_dir(): string {
|
||||
$dir = dirname(__DIR__, 2) . '/backups';
|
||||
@@ -113,26 +118,21 @@ function backup_trigger(string $type): bool {
|
||||
// Where the self-call lands. Default assumes shared/ is inside the document
|
||||
// root. Under a public/ docroot it is not, so hosts point this at a thin
|
||||
// wrapper instead. Constant wins (fotografia defines it in
|
||||
// config/functions_backup.php); .env covers hosts with no such file (surf).
|
||||
// config/functions_backup.php); .env covers hosts with no such file.
|
||||
$prefix = defined('BACKUP_TRIGGER_URL_PREFIX')
|
||||
? BACKUP_TRIGGER_URL_PREFIX
|
||||
: (function_exists('env')
|
||||
? ((string) (env('BACKUP_TRIGGER_URL_PREFIX') ?: 'shared/platform-tools/backups/'))
|
||||
: 'shared/platform-tools/backups/');
|
||||
// Host base-URL helpers differ: fotografia and the config/-stack sites
|
||||
// expose base_url(); surf exposes getBaseUrl(). Calling base_url()
|
||||
// unconditionally fataled with "undefined function" on the latter, before
|
||||
// any backup could be triggered. Fall back to APP_BASE_URL from .env so a
|
||||
// host with neither helper still works.
|
||||
// base_url() when the host has one; APP_BASE_URL from .env otherwise, so a
|
||||
// host without the helper still works.
|
||||
$path = ltrim($prefix . $script, '/');
|
||||
if (function_exists('base_url')) {
|
||||
$url = base_url($path);
|
||||
} elseif (function_exists('getBaseUrl')) {
|
||||
$url = rtrim(getBaseUrl(), '/') . '/' . $path;
|
||||
} else {
|
||||
$base = function_exists('env') ? (string) env('APP_BASE_URL', '') : (string) getenv('APP_BASE_URL');
|
||||
if ($base === '') {
|
||||
error_log('backup_trigger: no base URL available (no base_url(), no getBaseUrl(), no APP_BASE_URL).');
|
||||
error_log('backup_trigger: no base URL available (no base_url(), no APP_BASE_URL).');
|
||||
return false;
|
||||
}
|
||||
$url = rtrim($base, '/') . '/' . $path;
|
||||
@@ -165,6 +165,152 @@ function backup_trigger(string $type): bool {
|
||||
return $ok;
|
||||
}
|
||||
|
||||
// ─── Retenção avô-pai-filho ─────────────────────────────────────────────────
|
||||
//
|
||||
// Antes: tudo o que tivesse mais de BACKUP_KEEP_DAYS dias era apagado, e tudo o
|
||||
// que tivesse menos ficava — incluindo cinco cópias do mesmo dia (a da noite, a
|
||||
// de antes de atualizar, as do hub, as de segurança de um restauro). Num site
|
||||
// com galerias são gigabytes por cópia.
|
||||
//
|
||||
// Agora, por tipo (db e ficheiros contam em separado):
|
||||
// • tudo das últimas 24 h — são os pontos de restauro de uma atualização;
|
||||
// • a mais recente de cada um dos últimos BACKUP_KEEP_DAYS dias com cópia;
|
||||
// • a mais recente de cada uma das últimas BACKUP_KEEP_WEEKS semanas;
|
||||
// • a mais recente de cada um dos últimos BACKUP_KEEP_MONTHS meses;
|
||||
// • e a mais recente de todas, sempre.
|
||||
//
|
||||
// Cada escalão começa onde o anterior acaba: as semanais são semanas mais
|
||||
// antigas do que a diária mais antiga, e as mensais meses mais antigos do que a
|
||||
// semanal mais antiga. Sem isto a «mensal» era a cópia mais recente do mês
|
||||
// corrente — que já é a diária de hoje — e BACKUP_KEEP_MONTHS=1 não guardava
|
||||
// nada. Com 7/4/1 cobre-se cerca de dois meses.
|
||||
//
|
||||
// «Dias com cópia», e não «dias de calendário»: se as cópias pararem durante um
|
||||
// mês, a poda não pode ficar sem nada para guardar — é exactamente quando as
|
||||
// antigas são as únicas que há.
|
||||
|
||||
function backup_retention_policy(): array {
|
||||
$read = static function (string $key, int $default): int {
|
||||
if (defined($key)) return max(0, (int) constant($key));
|
||||
$v = function_exists('env') ? env($key, null) : (getenv($key) !== false ? getenv($key) : null);
|
||||
return ($v === null || $v === '') ? $default : max(0, (int) $v);
|
||||
};
|
||||
return [
|
||||
'recent_hours' => 24,
|
||||
'days' => $read('BACKUP_KEEP_DAYS', 7),
|
||||
'weeks' => $read('BACKUP_KEEP_WEEKS', 4),
|
||||
'months' => $read('BACKUP_KEEP_MONTHS', 1),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A data da cópia pelo nome do ficheiro, que não muda quando o ficheiro é
|
||||
* copiado ou restaurado de outro disco — ao contrário do mtime.
|
||||
*/
|
||||
function backup_created_at(string $filename, int $fallback): int {
|
||||
if (preg_match('/_(\d{2})_(\d{2})_(\d{4})_(\d{2})_(\d{2})(?:_(\d{2}))?(?:_[0-9a-f]{6})?\.zip$/', $filename, $m)) {
|
||||
$ts = mktime((int) $m[4], (int) $m[5], (int) ($m[6] ?? 0), (int) $m[2], (int) $m[1], (int) $m[3]);
|
||||
if ($ts !== false) return $ts;
|
||||
}
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Que cópias ficam. Função pura: recebe a lista (filename, type, created_at) e
|
||||
* devolve [filename => motivo]. O que não vier na resposta é para apagar.
|
||||
*/
|
||||
function backup_gfs_keep(array $items, array $policy, int $now): array {
|
||||
$tiers = [
|
||||
'days' => ['Y-m-d', 'diária'],
|
||||
'weeks' => ['o-\WW', 'semanal'],
|
||||
'months' => ['Y-m', 'mensal'],
|
||||
];
|
||||
|
||||
$byType = [];
|
||||
foreach ($items as $it) $byType[$it['type']][] = $it;
|
||||
|
||||
$keep = [];
|
||||
foreach ($byType as $list) {
|
||||
usort($list, fn($a, $b) => $b['created_at'] <=> $a['created_at']);
|
||||
$keep[$list[0]['filename']] = 'mais recente';
|
||||
|
||||
foreach ($list as $it) {
|
||||
if ($now - (int) $it['created_at'] < $policy['recent_hours'] * 3600) {
|
||||
$keep[$it['filename']] ??= 'últimas ' . $policy['recent_hours'] . ' h';
|
||||
}
|
||||
}
|
||||
|
||||
// $floor: a cópia mais antiga guardada pelo escalão anterior. O escalão
|
||||
// seguinte só olha para o que é mais antigo, e salta o balde dela — esse
|
||||
// período já está representado.
|
||||
$floor = null;
|
||||
foreach ($tiers as $tier => [$fmt, $label]) {
|
||||
$skip = $floor === null ? null : date($fmt, $floor);
|
||||
$buckets = [];
|
||||
$oldest = null;
|
||||
// Da mais recente para a mais antiga: a primeira que cai num balde é
|
||||
// a mais recente desse dia/semana/mês.
|
||||
foreach ($list as $it) {
|
||||
$t = (int) $it['created_at'];
|
||||
if ($floor !== null && $t >= $floor) continue;
|
||||
if (count($buckets) >= $policy[$tier]) break;
|
||||
$key = date($fmt, $t);
|
||||
if ($key === $skip || isset($buckets[$key])) continue;
|
||||
$buckets[$key] = true;
|
||||
$keep[$it['filename']] ??= $label;
|
||||
$oldest = $t;
|
||||
}
|
||||
if ($oldest !== null) $floor = $oldest;
|
||||
}
|
||||
}
|
||||
return $keep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica a retenção à pasta de cópias. Com $dryRun não apaga nada — diz o que
|
||||
* apagaria. Devolve ['policy', 'kept' => [[filename, reason]], 'deleted', 'errors'].
|
||||
*/
|
||||
function backup_prune(bool $dryRun = false): array {
|
||||
$dir = backup_dir();
|
||||
$policy = backup_retention_policy();
|
||||
$now = time();
|
||||
|
||||
$items = [];
|
||||
foreach (backup_list() as $b) {
|
||||
$b['created_at'] = backup_created_at($b['filename'], (int) $b['created_at']);
|
||||
$items[] = $b;
|
||||
}
|
||||
$keep = backup_gfs_keep($items, $policy, $now);
|
||||
|
||||
$out = ['policy' => $policy, 'kept' => [], 'deleted' => [], 'errors' => []];
|
||||
foreach ($items as $b) {
|
||||
if (isset($keep[$b['filename']])) {
|
||||
$out['kept'][] = ['filename' => $b['filename'], 'reason' => $keep[$b['filename']]];
|
||||
} elseif ($dryRun || @unlink($dir . '/' . $b['filename'])) {
|
||||
$out['deleted'][] = $b['filename'];
|
||||
} else {
|
||||
$out['errors'][] = $b['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
// .sql de dumps interrompidos antes de irem para o ZIP: texto em claro que
|
||||
// não aparece na lista. Um dia chega para não apanhar um dump a decorrer.
|
||||
foreach (glob($dir . '/db_backup_*.sql') ?: [] as $f) {
|
||||
if (filemtime($f) < $now - 86400) {
|
||||
if ($dryRun || @unlink($f)) $out['deleted'][] = basename($f);
|
||||
else $out['errors'][] = basename($f);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Linha legível da política, para a página e para o log dos workers. */
|
||||
function backup_retention_summary(?array $policy = null): string {
|
||||
$p = $policy ?? backup_retention_policy();
|
||||
return sprintf('tudo das últimas %d h, e a mais recente de cada um dos últimos %d dias, %d semanas e %d meses',
|
||||
$p['recent_hours'], $p['days'], $p['weeks'], $p['months']);
|
||||
}
|
||||
|
||||
function backup_type_label(string $type): string {
|
||||
return match($type) {
|
||||
'db' => 'Base de dados',
|
||||
|
||||
Reference in new issue
Block a user