163 lines
11 KiB
PHP
163 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Services\VerifiedDocumentBinary;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Inertia\Inertia;
|
|
|
|
class EditorialArticleController extends Controller
|
|
{
|
|
public function installation(Request $request, \App\Services\EditorialInstallation $installation)
|
|
{
|
|
$plan = $installation->preview($request->user());
|
|
return $request->expectsJson() ? response()->json(['plan' => $plan])->header('Cache-Control', 'private, no-store')
|
|
: response()->view('management.editorial-installation', ['plan' => $plan, 'operationKey' => (string) \Illuminate\Support\Str::uuid(), 'receipt' => null])->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function install(Request $request, \App\Services\EditorialInstallation $installation)
|
|
{
|
|
$receipt = $installation->apply($request->user(), $request->all());
|
|
return $request->expectsJson() ? response()->json(['receipt' => $receipt])->header('Cache-Control', 'private, no-store')
|
|
: response()->view('management.editorial-installation', ['plan' => $installation->preview($request->user()), 'operationKey' => (string) \Illuminate\Support\Str::uuid(), 'receipt' => $receipt])->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function showMfa(Request $request)
|
|
{
|
|
return $this->confirmMfa($request);
|
|
}
|
|
|
|
public function confirmMfa(Request $request)
|
|
{
|
|
$actor = $request->user();
|
|
abort_unless($actor->two_factor_secret !== null && $actor->two_factor_confirmed_at !== null, 403, 'MFA configurado e confirmado necessário.');
|
|
if (app(\App\Services\PortalProfileAccess::class)->mfa($actor)) {
|
|
return redirect()->route('editorial.installation');
|
|
}
|
|
if ($request->isMethod('get')) {
|
|
return response()->view('management.editorial-mfa')->header('Cache-Control', 'private, no-store');
|
|
}
|
|
abort_if(array_diff(array_keys($request->all()), ['code', 'recovery_code', '_token']), 422, 'A conta e o destino são obtidos da sessão e do servidor.');
|
|
$data = $request->validate(['code' => 'nullable|string|max:32', 'recovery_code' => 'nullable|string|max:255']);
|
|
try {
|
|
$recovery = collect($actor->recoveryCodes())->first(fn ($code) => isset($data['recovery_code']) && hash_equals($code, $data['recovery_code']));
|
|
$valid = $recovery !== null || (!empty($data['code']) && app(\Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider::class)->verify(
|
|
\Laravel\Fortify\Fortify::currentEncrypter()->decrypt($actor->two_factor_secret), $data['code']
|
|
));
|
|
} catch (\Illuminate\Contracts\Encryption\DecryptException $e) {
|
|
$valid = false;
|
|
}
|
|
if (!$valid) {
|
|
event(new \Laravel\Fortify\Events\TwoFactorAuthenticationFailed($actor));
|
|
throw ValidationException::withMessages(['code' => 'Não foi possível confirmar o código MFA.']);
|
|
}
|
|
if ($recovery !== null) {
|
|
$actor->replaceRecoveryCode($recovery);
|
|
}
|
|
event(new \Laravel\Fortify\Events\ValidTwoFactorAuthenticationCodeProvided($actor));
|
|
$request->session()->regenerate();
|
|
return redirect()->route('editorial.installation')->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
private function installed(): void
|
|
{
|
|
abort_unless(Schema::hasColumn('portal_resources', 'editorial_content'), 503, 'Editor editorial ainda não instalado.');
|
|
}
|
|
|
|
private function publicResources()
|
|
{
|
|
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
|
->where('r.published', true)->where('r.rights_status', 'authorized')->where('d.rights_status', 'authorized')
|
|
->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at')
|
|
->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256');
|
|
}
|
|
|
|
public function edit(int $id)
|
|
{
|
|
$this->installed();
|
|
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource(request()->user(), $id);
|
|
$r = DB::table('portal_resources')->find($id);
|
|
abort_unless($r, 404);
|
|
$reading = app(\App\Services\EditorialReading::class);
|
|
$original = $reading->original($r);
|
|
$content = $reading->content($r, $original);
|
|
return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version, 'published' => (bool) $r->published],
|
|
'content' => $content, 'source' => $original ? array_intersect_key($original, array_flip(['source_url', 'published_at', 'modified_at', 'legacy_id', 'image_source_url'])) : null, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']),
|
|
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])])->toResponse(request())->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function save(Request $request, int $id)
|
|
{
|
|
$this->installed();
|
|
$data = $this->validateContent($request);
|
|
DB::transaction(function () use ($request, $id, $data) {
|
|
$r = DB::table('portal_resources')->where('id', $id)->lockForUpdate()->first();
|
|
abort_unless($r, 404);
|
|
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
|
abort_unless((int) $r->metadata_version === (int) $data['metadata_version'], 409, 'O artigo foi alterado noutra sessão. Recarregue antes de editar.');
|
|
if (! empty($data['image_resource_id'])) {
|
|
abort_unless($this->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
|
}
|
|
$body = trim($data['body']);
|
|
if ($body === '') {
|
|
throw ValidationException::withMessages(['body' => 'Escreva o corpo do artigo.']);
|
|
}
|
|
$content = $this->edition($r, $data);
|
|
$revision = (int) $r->metadata_version + 1;
|
|
DB::table('portal_resources')->where('id', $id)->update(['editorial_content' => json_encode($content, JSON_THROW_ON_ERROR), 'published' => false, 'approved_by' => null, 'metadata_version' => $revision, 'last_reviewed_by' => $request->user()->id, 'last_reviewed_at' => now(), 'updated_at' => now()]);
|
|
$resource = DB::table('portal_resources')->find($id);
|
|
$links = DB::table('portal_resource_links')->where('resource_id', $id)->orderBy('id')->get();
|
|
DB::table('portal_resource_revisions')->insert(['resource_id' => $id, 'revision_no' => $revision, 'change_type' => 'metadata_updated', 'snapshot' => json_encode(['resource' => $resource, 'associations' => $links], JSON_THROW_ON_ERROR), 'actor_user_id' => $request->user()->id, 'created_at' => now()]);
|
|
DB::table('audit_events')->insert(['source_system' => 'portal-management', 'actor_user_id' => $request->user()->id, 'entity_type' => 'portal_resources', 'entity_id' => null, 'action' => 'editorial_updated', 'before_data' => json_encode(['record_id' => $id, 'metadata_version' => $r->metadata_version]), 'after_data' => json_encode(['record_id' => $id, 'metadata_version' => $revision, 'published' => false])]);
|
|
});
|
|
|
|
return redirect('/management/portal-services/resources/'.$id.'/article')->with('success', 'Artigo guardado como rascunho. Publique expressamente no acervo quando estiver pronto.');
|
|
}
|
|
|
|
private function validateContent(Request $request): array
|
|
{
|
|
return $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'image_mode' => ['nullable', 'in:original,selected,none'], 'metadata_version' => ['required', 'integer', 'min:1']]);
|
|
}
|
|
|
|
private function edition(object $resource, array $data): array
|
|
{
|
|
$reading = app(\App\Services\EditorialReading::class);
|
|
$original = $reading->original($resource);
|
|
$mode = $data['image_mode'] ?? (!empty($data['image_resource_id']) ? 'selected' : 'none');
|
|
if ($mode === 'selected') abort_unless(!empty($data['image_resource_id']) && $reading->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
|
return ['format' => 1, 'body' => trim($data['body']), 'image_mode' => $mode, 'image_resource_id' => $mode === 'selected' ? (int)$data['image_resource_id'] : null,
|
|
'preserve_original_html' => $original && trim($data['body']) === $reading->plain($original['content_html'])];
|
|
}
|
|
|
|
public function preview(Request $request, int $id)
|
|
{
|
|
$this->installed();
|
|
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
|
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
|
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource, null, true)])
|
|
->toResponse($request)->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
|
}
|
|
|
|
public function previewContent(Request $request, int $id)
|
|
{
|
|
$this->installed();
|
|
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
|
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
|
$data = $this->validateContent($request);
|
|
abort_unless((int)$resource->metadata_version === (int)$data['metadata_version'], 409, 'A revisão mudou; recupere antes de rever a prévia.');
|
|
return response()->json(['article' => app(\App\Services\EditorialReading::class)->reading($resource, $this->edition($resource, $data), true)])
|
|
->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
|
}
|
|
|
|
public function show(Request $request, int $id)
|
|
{
|
|
$this->installed();
|
|
$resource = app(\App\Services\EditorialReading::class)->publicResources()->where('r.id', $id)->first(['r.*']);
|
|
abort_unless($resource, 404);
|
|
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource)])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
|
}
|
|
}
|