Files
Investidor-V2/app/Services/Malware/DocumentScanRetry.php
T

79 lines
4.5 KiB
PHP

<?php
namespace App\Services\Malware;
use App\Jobs\ScanDocumentForMalware;
use App\Models\User;
use App\Services\DocumentManagementAccess;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
final class DocumentScanRetry
{
/** @return array{outcome:string,reference:string} */
public function request(User $actor, string $id): array
{
$job = new ScanDocumentForMalware($id);
$outcome = 'deferred';
DB::transaction(function () use ($actor, $id, $job, &$outcome): void {
$document = app(DocumentManagementAccess::class)->documents($actor)->where('id', $id)->lockForUpdate()->first();
abort_unless($document && $document->status !== 'deleted' && $document->deleted_at === null, 404);
abort_unless(in_array($document->malware_scan_status, ['pending', 'inconclusive', 'failed'], true), 422,
'Só é possível repetir uma análise pendente, inconclusiva ou falhada. Para substituir um ficheiro, carregue uma nova versão.');
DB::afterCommit(function () use ($actor, $id, $job, $document, &$outcome): void {
$prepared = false;
try {
$outcome = app(DocumentScanDelivery::class)->dispatch($job, function () use ($actor, $id, $job, $document, &$prepared): bool {
$ready = DB::transaction(function () use ($actor, $id, $job, $document): bool {
$currentActor = $actor->fresh();
if (! $currentActor) {
return false;
}
$access = app(DocumentManagementAccess::class);
if (! $access->full($currentActor) && ! $access->limited($currentActor) && ! $access->editorial($currentActor)) {
return false;
}
$current = $access->documents($currentActor)->where('id', $id)->lockForUpdate()->first();
if (! $current || $current->status === 'deleted' || $current->deleted_at !== null
|| $current->sha256 !== $document->sha256 || $current->malware_scan_attempts !== $document->malware_scan_attempts
|| $current->malware_scan_status !== $document->malware_scan_status || $current->updated_at !== $document->updated_at) {
return false;
}
DB::table('documents')->where('id', $id)->update(['status' => 'quarantined', 'malware_scan_status' => 'pending', 'malware_failure_reason' => null, 'updated_at' => now()]);
$access->audit($currentActor, 'scan_retry_requested', ['document_id' => $id, 'reference' => $job->runIdentifier(), 'previous_scan_status' => $current->malware_scan_status]);
return true;
});
$prepared = $ready;
return $ready;
});
} catch (Throwable) {
$outcome = 'unconfirmed';
try {
if ($prepared) {
DB::transaction(function () use ($id, $document): void {
$current = DB::table('documents')->where('id', $id)->lockForUpdate()->first();
if ($current && $current->deleted_at === null && $current->status !== 'deleted'
&& $current->sha256 === $document->sha256 && $current->malware_scan_attempts === $document->malware_scan_attempts && $current->malware_scan_status === 'pending') {
DB::table('documents')->where('id', $id)->update(['status' => 'quarantined', 'malware_scan_status' => 'failed', 'malware_failure_reason' => 'queue_dispatch_failed', 'updated_at' => now()]);
}
});
}
} catch (Throwable) {
// An uncertain write remains private and needs reconciliation.
}
try {
Log::warning('Document scan retry dispatch requires verification.', ['reference' => $job->runIdentifier(), 'error_code' => 'scan_dispatch_unconfirmed']);
} catch (Throwable) {
}
}
});
});
return ['outcome' => $outcome, 'reference' => $job->runIdentifier()];
}
}