101 lines
7.6 KiB
PHP
101 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
/** An existing opportunity resource references the original document. No binary or permission is copied. */
|
|
class OpportunityLibrarySupport
|
|
{
|
|
public function choices(User $actor): array
|
|
{
|
|
return $this->sources($actor)->orderBy('r.id')->limit(100)->get(['r.id', 'r.title', 'r.version_label', 'r.metadata_version', 'r.published', 'd.sha256'])->all();
|
|
}
|
|
|
|
private function sources(User $actor)
|
|
{
|
|
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
|
->whereNull('d.deleted_at')->where('d.status', '!=', 'deleted')
|
|
->where('r.rights_status', '!=', 'restricted')->where('d.rights_status', '!=', 'restricted')
|
|
->whereIn('d.data_classification', ['internal', 'public'])
|
|
->when(! $actor->can('manage-portal'), function ($query) use ($actor) {
|
|
$query->where(function ($scope) use ($actor) {
|
|
$scope->where(fn ($public) => $public->where('r.published',true)->where('d.data_classification','public')->where('d.status','active')->where('d.malware_scan_status','clean')->whereColumn('d.sha256','d.malware_scanned_sha256'));
|
|
if ($actor->hasVerifiedEmail()) {
|
|
$scope->orWhere(fn ($own) => $own->where('r.created_by',$actor->id)->where('d.uploaded_by',$actor->id));
|
|
}
|
|
$editorial=app(DocumentManagementAccess::class);
|
|
if ($editorial->editorial($actor)) {
|
|
$scope->orWhereIn('r.id',$editorial->editorialResources($actor)->select('r.id'));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public function attach(User $actor, Opportunity $opportunity, array $input): \stdClass
|
|
{
|
|
app(PortalProfileAccess::class)->authorize($actor, 'edit', $opportunity);
|
|
$data = Validator::make($input, ['resource_id' => 'required|integer|min:1', 'creation_key' => 'required|uuid', 'etag' => 'required|string|size:64', 'reason' => 'required|string|min:20|max:1000', 'section' => 'nullable|string|max:255', 'supported_statement' => 'nullable|string|max:2000'])->validate();
|
|
|
|
return DB::transaction(function () use ($actor, $opportunity, $data) {
|
|
$opportunity = Opportunity::whereKey($opportunity->id)->lockForUpdate()->firstOrFail();
|
|
app(PortalProfileAccess::class)->authorize($actor, 'edit', $opportunity);
|
|
$source = $this->sources($actor)->where('r.id', $data['resource_id'])->lockForUpdate()->first(['r.*', 'd.id as original_id', 'd.sha256', 'd.version_no']);
|
|
abort_unless($source, 403, 'O acesso actual à biblioteca não permite reutilizar este original.');
|
|
$existing = DB::table('opportunity_documents')->where('opportunity_id', $opportunity->id)->whereNull('deleted_at')->where('document_id', $source->original_id)->first();
|
|
if ($existing) {
|
|
$payload = json_decode($existing->payload, true);
|
|
abort_unless(($payload['library_source']['resource_id'] ?? null) === $source->id, 409, 'Original já associado por outro percurso; recuperar o suporte existente.');
|
|
abort_unless(($payload['section'] ?? null) === ($data['section'] ?? null) && ($payload['supported_statement'] ?? null) === ($data['supported_statement'] ?? null), 409, 'Original já associado com outra referência; editar explicitamente o suporte existente.');
|
|
return $existing;
|
|
}
|
|
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($opportunity), $data['etag']), 409, 'A ficha mudou; preserve a selecção e recupere a versão actual.');
|
|
abort_if(DB::table('opportunity_documents')->where('id', $data['creation_key'])->exists(), 409, 'Chave já utilizada noutro suporte.');
|
|
abort_if(DB::table('opportunity_documents')->where('opportunity_id', $opportunity->id)->whereNull('deleted_at')->count() >= 100, 422, 'Limite de documentos atingido.');
|
|
$payload = ['title' => $source->title, 'category' => $source->category, 'issuer' => null, 'edition' => $source->version_label,
|
|
'classification' => 'internal', 'visibility' => 'internal', 'source_reference' => $source->source_reference,
|
|
'section' => $data['section'] ?? null, 'supported_statement' => $data['supported_statement'] ?? null,
|
|
'library_source' => ['resource_id' => $source->id, 'document_id' => $source->original_id, 'metadata_version' => $source->metadata_version,
|
|
'document_version' => $source->version_no, 'edition' => $source->version_label, 'sha256' => trim($source->sha256), 'source_reference' => $source->source_reference]];
|
|
$now = now();
|
|
DB::table('opportunity_documents')->insert(['id' => $data['creation_key'], 'opportunity_id' => $opportunity->id, 'document_id' => $source->original_id,
|
|
'payload' => json_encode($payload, JSON_THROW_ON_ERROR), 'version' => 1, 'created_by' => $actor->id, 'created_at' => $now, 'updated_at' => $now]);
|
|
$after = DB::table('opportunity_documents')->where('id', $data['creation_key'])->first();
|
|
DB::table('audit_events')->insert(['source_system' => 'opportunity-resources', 'entity_type' => 'documents', 'actor_user_id' => $actor->id,
|
|
'action' => 'library_original_reused', 'data_classification' => 'restricted', 'before_data' => json_encode(['snapshot' => Crypt::encryptString('null')]),
|
|
'after_data' => json_encode(['opportunity_id' => $opportunity->id, 'resource_id' => $after->id, 'version' => 1,
|
|
'reason' => Crypt::encryptString($data['reason']), 'snapshot' => Crypt::encryptString(json_encode($after, JSON_THROW_ON_ERROR))])]);
|
|
$opportunity->management_version++;
|
|
$opportunity->save();
|
|
return $after;
|
|
});
|
|
}
|
|
|
|
public function assertAvailable(\stdClass $resource, \stdClass $document): void
|
|
{
|
|
$payload = is_string($resource->payload) ? json_decode($resource->payload, true) : $resource->payload;
|
|
$pin = $payload['library_source'] ?? null;
|
|
if (! $pin) return;
|
|
$source = DB::table('portal_resources')->where('id', $pin['resource_id'])->first();
|
|
abort_unless($source && $source->rights_status !== 'restricted' && $document->rights_status !== 'restricted'
|
|
&& in_array($document->data_classification, ['internal', 'public'], true)
|
|
&& $document->id === $pin['document_id'] && hash_equals($pin['sha256'], trim($document->sha256)), 403, 'O original está retirado, restrito ou já não corresponde à versão guardada.');
|
|
}
|
|
|
|
public function snapshot(User $actor, Opportunity $opportunity): array
|
|
{
|
|
$access = app(PortalProfileAccess::class);
|
|
if (! $access->canView($actor, $opportunity)) return [];
|
|
return collect(app(OpportunityResources::class)->listing($opportunity->id, 'documents'))->filter(fn ($r) => ! $r->deleted_at && isset($r->payload['library_source']))->map(function ($r) use ($opportunity) {
|
|
$pin = $r->payload['library_source'];
|
|
return [...$pin, 'title' => $r->payload['title'], 'support_id' => $r->id, 'support_version' => $r->version,
|
|
'section' => $r->payload['section'] ?? null, 'opportunity_id' => $opportunity->id,
|
|
'download_url' => '/minhas-oportunidades/'.$opportunity->id.'/resources/documents/'.$r->id.'/download?source_sha256='.$pin['sha256']];
|
|
})->values()->all();
|
|
}
|
|
}
|