157 lines
5.6 KiB
PHP
157 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Livewire\Settings\XmlExchange;
|
|
use App\Services\BackupArchive;
|
|
use App\Services\BackupOperationState;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Ripristina un pacchetto di backup in coda, riportando l'avanzamento fase per
|
|
* fase (estrazione, database, file).
|
|
*/
|
|
class RestoreBackupArchive implements ShouldQueue
|
|
{
|
|
use Dispatchable;
|
|
use InteractsWithQueue;
|
|
use Queueable;
|
|
use SerializesModels;
|
|
|
|
public int $tries = 1;
|
|
|
|
public int $timeout = 570;
|
|
|
|
/** Chiave con cui la schermata di cambio password ritrova l'esito. */
|
|
public const CHIAVE_ESITO = 'restore-esito:';
|
|
|
|
public function __construct(
|
|
public string $operationId,
|
|
public string $zipPath,
|
|
public int $actorId,
|
|
) {
|
|
}
|
|
|
|
public function handle(BackupOperationState $stato, BackupArchive $archivio, XmlExchange $exchange): void
|
|
{
|
|
$stato->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();
|
|
}
|
|
}
|