markRunning($this->operationId); $staging = null; try { $stato->fase($this->operationId, 'Estrazione dell\'archivio'); $estratto = $archivio->extract( $this->zipPath, fn (int $fatti, int $totale) => $stato->avanza($this->operationId, $fatti, $totale) ); $staging = $estratto['staging']; $stato->fase($this->operationId, 'Riscrittura del database', 1); // importXmlFromContent() attribuisce a auth()->id() le assegnazioni // il cui autore non e' nel file. Qui non c'e' sessione, quindi // l'utente va reimpostato a mano: senza, l'audit finirebbe sul // primo id disponibile invece che su chi ha avviato il ripristino. Auth::onceUsingId($this->actorId); $result = $exchange->importXmlFromContent($estratto['xml']); if (isset($result['error'])) { $stato->markFailed($this->operationId, $result['error']); return; } $stato->avanza($this->operationId, 1, 1); $stato->fase($this->operationId, 'Ripristino dei file'); $fileRipristinati = $archivio->restoreFiles( $staging, fn (int $fatti, int $totale) => $stato->avanza($this->operationId, $fatti, $totale) ); $stato->fase($this->operationId, 'Blocco degli account', 1); $utenti = $this->richiediCambioPasswordATutti(); // L'import ricrea ruoli e assegnazioni, ma Spatie tiene in cache la // mappa permessi-ruoli: senza questo, i processi che l'hanno gia' // caricata continuano a decidere gli accessi con quella di prima // del ripristino, finche' la cache non scade da sola. app(PermissionRegistrar::class)->forgetCachedPermissions(); $stato->avanza($this->operationId, 1, 1); } catch (\Throwable $e) { Log::error('Ripristino del backup non riuscito.', ['exception' => $e]); $stato->markFailed($this->operationId, $e->getMessage()); return; } finally { Auth::forgetUser(); if ($staging !== null) { $archivio->deleteDirectory($staging); } @unlink($this->zipPath); } $stats = $result['stats']; $messaggio = sprintf( 'Ripristino completato: %d territori, %d proclamatori, %d assegnazioni e %d file ripristinati. ' . 'Per sicurezza %s impostare una nuova password.', $stats['territori_importati'] ?? 0, $stats['proclamatori_importati'] ?? 0, $stats['assegnazioni_importate'] ?? 0, $fileRipristinati, $utenti === 1 ? 'l\'unico account deve' : 'tutti i ' . $utenti . ' account devono' ); // Il messaggio non puo' viaggiare in sessione (qui non ce n'e' una) e la // pagina che dovra' mostrarlo non e' questa: appena il flag e' acceso, // il middleware dirotta l'amministratore sul cambio password. Lo si // lascia in cache a suo nome, e quella schermata lo consuma. Cache::put(self::CHIAVE_ESITO . $this->actorId, $messaggio, 3600); $stato->log($this->operationId, $messaggio); $stato->markCompleted($this->operationId, [ 'stats' => $stats, 'issues' => array_slice($result['issues'], 0, 50), 'file_ripristinati' => $fileRipristinati, 'messaggio' => $messaggio, ]); } /** * Vedi CreateBackupArchive::failed(): chiude l'operazione quando il job * muore per cause che handle() non intercetta, cosi' il posto unico si * libera subito invece di restare occupato fino alla scadenza. * * Qui il messaggio dice anche in che stato si e' rimasti: un ripristino * interrotto a meta' e' l'unico caso in cui l'applicazione puo' trovarsi * con database e file disallineati, e chi legge deve sapere che la strada * e' rilanciare il ripristino, non ignorare l'errore. */ public function failed(?\Throwable $e): void { app(BackupOperationState::class)->markFailed( $this->operationId, ($e?->getMessage() ?: 'Ripristino interrotto (timeout o errore del worker).') . ' I dati possono essere rimasti a meta\': rilancia il ripristino con lo stesso pacchetto.' ); @unlink($this->zipPath); } /** * Blocca ogni account finche' non sceglie una password nuova, * l'amministratore che sta ripristinando compreso. * * Il pacchetto contiene gli hash bcrypt di tutti gli utenti: chi ha avuto * il file fra le mani ha potuto tentarne il crack con calma, quindi dopo un * ripristino nessuna delle vecchie password si puo' considerare segreta. * Anche i remember_token vengono azzerati: sono credenziali persistenti a * tutti gli effetti e stanno pure loro nel backup. * * Scritto con il query builder di proposito — un update di massa su * Eloquent qui non aggiungerebbe nulla e toccherebbe updated_at su tutte le * righe appena importate con i loro timestamp originali. * * @return int Account bloccati. Non e' il valore di ritorno dell'update: * MariaDB conta le righe *cambiate*, quindi un utente gia' * marcato da un ripristino precedente non comparirebbe e il * riepilogo direbbe meno account di quanti ne sono bloccati. */ private function richiediCambioPasswordATutti(): int { DB::table('users')->update([ 'password_change_required' => true, 'remember_token' => null, ]); return (int) DB::table('users')->count(); } }