79 lines
4.5 KiB
PHP
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()];
|
|
}
|
|
}
|