117 lines
7.8 KiB
PHP
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;
|
|
}
|
|
}
|