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(); $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, ]); } /** * 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(); } }