Use verified CLI preparations when web PHP cannot execute processes
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -13,6 +13,48 @@ use Symfony\Component\Process\Process;
|
||||
|
||||
final class DemographicImport
|
||||
{
|
||||
/**
|
||||
* CLI preparation is read-only input, never an import or approval.
|
||||
* @return array{preview:string,records:string,receipt:array<string,mixed>}|null
|
||||
*/
|
||||
public function preparedSource(string $sha, ?int $baselineYear, bool $verifyRecords = true): ?array
|
||||
{
|
||||
$root = config('demography.prepared_directory');
|
||||
if (! is_string($root) || $root === '') { return null; }
|
||||
abort_unless(preg_match('/^[a-f0-9]{64}$/', $sha) === 1, 422);
|
||||
$directory = $root.'/'.$sha;
|
||||
if (! is_file($directory.'/preparation.json')) { return null; }
|
||||
abort_unless(is_readable($directory.'/preparation.json'), 409, 'Recibo CLI indisponível.');
|
||||
try { $receipt = json_decode(CheckedFile::read($directory.'/preparation.json'), true, 512, JSON_THROW_ON_ERROR); }
|
||||
catch (\JsonException) { abort(409, 'Recibo CLI inválido.'); }
|
||||
abort_unless(is_array($receipt), 409, 'Recibo CLI inválido.');
|
||||
$preview = $directory.'/preview.json';
|
||||
$records = $directory.'/records.ndjson';
|
||||
abort_unless(($receipt['sha256'] ?? '') === $sha && array_key_exists('baseline_year', $receipt) && $receipt['baseline_year'] === $baselineYear
|
||||
&& ($receipt['parser_sha256'] ?? '') === CheckedFile::sha256(base_path('tools/demography_workbook.py'))
|
||||
&& is_file($preview) && CheckedFile::matches($preview, $receipt['preview_sha256'] ?? '') && is_file($records), 409, 'A preparação CLI mudou; conservar o original e preparar novamente fora do servidor web.');
|
||||
try { $report = json_decode(CheckedFile::read($preview), true, 512, JSON_THROW_ON_ERROR); }
|
||||
catch (\JsonException) { abort(409, 'Prévia CLI inválida.'); }
|
||||
abort_unless(is_array($report), 409, 'Prévia CLI inválida.');
|
||||
abort_unless(($report['sha256'] ?? '') === $sha && ($report['record_count'] ?? null) === ($receipt['record_count'] ?? null)
|
||||
&& ($report['records_file_sha256'] ?? '') === ($receipt['records_sha256'] ?? '')
|
||||
&& (! $verifyRecords || CheckedFile::matches($records, $receipt['records_sha256'] ?? '')), 409, 'Resultados da preparação CLI alterados.');
|
||||
|
||||
return ['preview' => $preview, 'records' => $records, 'receipt' => $receipt];
|
||||
}
|
||||
|
||||
/** @param array<string,array<string,mixed>> $files */
|
||||
public function preparedSourcesReady(array $files): bool
|
||||
{
|
||||
try {
|
||||
foreach ($files as $name => $expected) {
|
||||
$source = $this->preparedSource($expected['sha256'], $name === 'Dados demograficos.xlsx' ? 2017 : null, false);
|
||||
if ($source === null || $source['receipt']['record_count'] !== $expected['records']) { return false; }
|
||||
}
|
||||
return $files !== [];
|
||||
} catch (\Throwable) { return false; }
|
||||
}
|
||||
|
||||
public function authorize(User $actor): void
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
@@ -43,13 +85,21 @@ final class DemographicImport
|
||||
$scan = app(MalwareScanner::class)->scan($path, (int) filesize($path), $disk->path($directory));
|
||||
abort_unless($scan['result'] === 'clean', 423, 'Análise antimalware pendente ou não limpa; original privado conservado.');
|
||||
$output = $disk->path($directory.'/preview.json');
|
||||
$command = [config('demography.python', 'python'), base_path('tools/demography_workbook.py'), $path, '--output', $output, '--records-output', $disk->path($directory.'/records.ndjson')];
|
||||
if ($baselineYear !== null) {
|
||||
array_push($command, '--baseline-year', (string) $baselineYear);
|
||||
$prepared = $this->preparedSource($sha, $baselineYear);
|
||||
if ($prepared !== null) {
|
||||
abort_unless(copy($prepared['preview'], $output) && CheckedFile::matches($output, $prepared['receipt']['preview_sha256'])
|
||||
&& copy($prepared['records'], $disk->path($directory.'/records.ndjson'))
|
||||
&& CheckedFile::matches($disk->path($directory.'/records.ndjson'), $prepared['receipt']['records_sha256']), 409, 'Preparação CLI alterada durante a cópia.');
|
||||
} else {
|
||||
abort_unless(function_exists('proc_open'), 503, 'Este servidor web não executa o preparador. Prepare este original pelo importador CLI e confira o respectivo recibo antes de continuar.');
|
||||
$command = [config('demography.python', 'python'), base_path('tools/demography_workbook.py'), $path, '--output', $output, '--records-output', $disk->path($directory.'/records.ndjson')];
|
||||
if ($baselineYear !== null) {
|
||||
array_push($command, '--baseline-year', (string) $baselineYear);
|
||||
}
|
||||
$process = new Process($command, base_path(), timeout: 120);
|
||||
$process->run();
|
||||
abort_unless($process->isSuccessful(), 422, 'Não foi possível reconhecer o Excel; confira a estrutura e o ano de referência.');
|
||||
}
|
||||
$process = new Process($command, base_path(), timeout: 120);
|
||||
$process->run();
|
||||
abort_unless($process->isSuccessful(), 422, 'Não foi possível reconhecer o Excel; confira a estrutura e o ano de referência.');
|
||||
$report = json_decode(CheckedFile::read($output), true, 512, JSON_THROW_ON_ERROR);
|
||||
abort_unless(hash_equals($sha, $report['sha256']) && CheckedFile::matches($path, $sha), 409);
|
||||
$summary = array_diff_key($report, ['records' => true]);
|
||||
|
||||
Reference in New Issue
Block a user