Files

117 lines
7.8 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
final class DeskAttachments
{
public const MIME_TYPES = ['application/pdf', 'image/png', 'image/jpeg', 'text/plain'];
public function scopedLinksInstalled(): bool
{
$indexes = DB::selectOne("SELECT to_regclass('public.desk_documents_scope_unique') AS scope, to_regclass('public.desk_documents_dossier_unique') AS dossier");
return $indexes->scope !== null && $indexes->dossier !== null;
}
/** @param array<string,mixed> $definition
* @return array<string,mixed> */
public function field(array $definition, string $key): array
{
$field = $definition['fields'][$key] ?? null;
if (! $field || $field['type'] !== 'attachment') {
throw ValidationException::withMessages(['field_key' => 'Seleccione uma pergunta de anexo da versão deste pedido.']);
}
return $field;
}
/** @param array<string,mixed> $field */
public function checkMetadata(\stdClass $document, array $field, string $path, bool $allowUnknown = false): void
{
$mime = strtolower(trim(explode(';', (string) ($document->media_type ?? ''), 2)[0]));
if (($mime === '' && ! $allowUnknown) || ($mime !== '' && ! in_array($mime, $field['mime_types'], true)) || ($document->size_bytes === null && ! $allowUnknown) || ($document->size_bytes !== null && ($document->size_bytes === false || $document->size_bytes < 0 || $document->size_bytes > $field['max_size_kb'] * 1024))) {
throw ValidationException::withMessages([$path => 'O ficheiro não corresponde aos formatos ou ao tamanho máximo desta pergunta. Escolha outro suporte; o original é conservado.']);
}
}
/** Validate references through the existing case/request boundary; no parallel document store.
* @param array<string,mixed> $payload
* @param array<string,mixed> $definition
* @return array<string,mixed> */
public function snapshot(\stdClass $request, array $payload, array $definition, bool $submitting): array
{
$applicable = app(VirtualDesk::class)->applicableFields($payload, $definition);
$snapshot = [];
$verified = [];
foreach ($definition['fields'] as $key => $field) {
if ($field['type'] !== 'attachment') {
continue;
}
$files = [];
$seen = [];
foreach ($payload[$key] ?? [] as $linkId) {
$document = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')
->where('l.id', $linkId)->where('l.case_id', $request->case_id)->whereNull('d.deleted_at')
->where(fn ($q) => $q->whereNull('l.request_id')->orWhere('l.request_id', $request->id))
->first(['d.*', 'l.id as link_id', 'l.source_reference', 'l.category', 'l.valid_until as declared_valid_until', 'l.stage as linked_stage']);
if (! $document) {
throw ValidationException::withMessages(['payload.'.$key => 'Seleccione documentos associados a este dossier e disponíveis neste pedido.']);
}
if (isset($seen[$document->id])) {
throw ValidationException::withMessages(['payload.'.$key => 'Seleccione cada ficheiro apenas uma vez nesta pergunta. Pode reutilizá-lo noutras perguntas aplicáveis.']);
}
$seen[$document->id] = true;
$active = isset($applicable[$key]);
// Hidden answers are preserved; they are not accepted as applicable evidence.
if ($active) {
$this->checkMetadata($document, $field, 'payload.'.$key, true);
}
$usable = $document->status === 'active' && $document->malware_scan_status === 'clean'
&& hash_equals(trim($document->sha256), trim((string) $document->malware_scanned_sha256));
if ($submitting && $active) {
if (! $usable) {
throw ValidationException::withMessages(['payload.'.$key => 'O suporte seleccionado ainda não está disponível com análise de segurança válida. Actualize o estado dos documentos antes de submeter.']);
}
if (! isset($verified[$document->id])) {
try {
$verificationDocument = clone $document;
$verificationDocument->media_type = $verificationDocument->media_type ?: 'application/octet-stream';
$response = app(VerifiedDocumentBinary::class)->respond($verificationDocument);
if ($response instanceof BinaryFileResponse) {
$verified[$document->id] = ['size_bytes' => $response->getFile()->getSize(), 'media_type' => (new \finfo(FILEINFO_MIME_TYPE))->file($response->getFile()->getPathname())];
} else {
$content = $response->getContent();
if (! is_string($content)) {
throw ValidationException::withMessages(['payload.'.$key => 'Não foi possível conferir o original deste suporte.']);
}
$verified[$document->id] = ['size_bytes' => strlen($content), 'media_type' => (new \finfo(FILEINFO_MIME_TYPE))->buffer($content)];
}
} catch (HttpExceptionInterface $error) {
throw ValidationException::withMessages(['payload.'.$key => 'O suporte não está disponível com a autorização e integridade exigidas. Recupere ou seleccione outro documento.']);
}
}
$this->checkMetadata((object) $verified[$document->id], $field, 'payload.'.$key);
$recordedMime = strtolower(trim(explode(';', (string) ($document->media_type ?? ''), 2)[0]));
if (($document->size_bytes !== null && $verified[$document->id]['size_bytes'] !== (int) $document->size_bytes) || ($recordedMime !== '' && $verified[$document->id]['media_type'] !== $recordedMime)) {
throw ValidationException::withMessages(['payload.'.$key => 'O formato ou tamanho do original diverge dos dados registados. Recupere ou seleccione outro documento.']);
}
}
$measured = $submitting && $active ? $verified[$document->id] : null;
$files[] = ['link_id' => $document->link_id, 'document_id' => $document->id, 'file_name' => $document->file_name,
'sha256' => trim($document->sha256), 'version_no' => $document->version_no, 'media_type' => $measured['media_type'] ?? $document->media_type,
'size_bytes' => $measured['size_bytes'] ?? $document->size_bytes, 'recorded_media_type' => $document->media_type, 'recorded_size_bytes' => $document->size_bytes, 'metadata_measured' => $measured !== null, 'source_reference' => $document->source_reference,
'valid_until' => $document->declared_valid_until, 'stage' => $document->linked_stage, 'status' => $document->status, 'malware_scan_status' => $document->malware_scan_status, 'usable_at_submission' => $submitting && $active && $usable];
}
$snapshot[$key] = ['label' => $field['label'], 'applicable' => isset($applicable[$key]), 'service_version_id' => $request->service_version_id,
'limits' => ['mime_types' => $field['mime_types'], 'max_size_kb' => $field['max_size_kb'], 'max_files' => $field['max_files']], 'files' => $files];
}
return $snapshot;
}
}