131 lines
7.3 KiB
PHP
131 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use GuzzleHttp\Psr7\Uri;
|
|
use GuzzleHttp\Psr7\UriResolver;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
/** Reads verified originals without writing a derived edition or changing publication. */
|
|
final class EditorialReading
|
|
{
|
|
public 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 original(object $resource): ?array
|
|
{
|
|
$document = DB::table('documents')->find($resource->document_id);
|
|
if (! $document || $document->status !== 'active' || $document->malware_scan_status !== 'clean' || ! in_array($document->media_type, ['text/plain', 'application/json'], true)) {
|
|
return null;
|
|
}
|
|
$binary = app(VerifiedDocumentBinary::class)->respond($document);
|
|
$bytes = $binary instanceof BinaryFileResponse ? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
|
|
if (! is_string($bytes) || strlen($bytes) > 10485760) {
|
|
return null;
|
|
}
|
|
$original = json_decode($bytes, true);
|
|
|
|
return is_array($original) && ($original['format'] ?? '') === 'investmoz-legacy-article-v1' && is_string($original['content_html'] ?? null) ? $original : null;
|
|
}
|
|
|
|
public function plain(string $html): string
|
|
{
|
|
return trim(html_entity_decode(strip_tags(preg_replace('~</(?:p|div|li|h[1-6]|tr)>|<br\s*/?>~i', "\n\n", $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
|
}
|
|
|
|
public function content(object $resource, ?array $original): array
|
|
{
|
|
return $resource->editorial_content ? json_decode($resource->editorial_content, true, 512, JSON_THROW_ON_ERROR)
|
|
: ['body' => $this->plain($original['content_html'] ?? ''), 'image_resource_id' => null, 'image_mode' => 'original', 'preserve_original_html' => true];
|
|
}
|
|
|
|
private function sourceUrl(mixed $url, ?string $base = null): ?string
|
|
{
|
|
if (! is_string($url) || trim($url) === '') {
|
|
return null;
|
|
}
|
|
try {
|
|
$absolute = $base ? (string) UriResolver::resolve(new Uri($base), new Uri($url)) : $url;
|
|
|
|
return filter_var($absolute, FILTER_VALIDATE_URL) && in_array(strtolower((string) parse_url($absolute, PHP_URL_SCHEME)), ['http', 'https'], true) ? $absolute : null;
|
|
} catch (\InvalidArgumentException) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public function reading(object $resource, ?array $override = null, bool $private = false): array
|
|
{
|
|
$original = $this->original($resource);
|
|
$content = $override ?? $this->content($resource, $original);
|
|
abort_unless(trim($content['body'] ?? '') !== '', 404, 'Este documento não contém um artigo legível.');
|
|
$document = DB::table('documents')->find($resource->document_id);
|
|
// The original is verified above; ordinary text adaptations retain the binary gate too.
|
|
app(VerifiedDocumentBinary::class)->respond($document);
|
|
$mode = $content['image_mode'] ?? (! empty($content['image_resource_id']) ? 'selected' : 'none');
|
|
$originalImageUrl = $this->sourceUrl($original['image_source_url'] ?? null, $original['source_url'] ?? null);
|
|
$expectedImageSha = $original['image_sha256'] ?? null;
|
|
if (! $expectedImageSha && $originalImageUrl === 'https://investemoz.tempo.co.mz/images/tempo-background.png') {
|
|
foreach (json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR) as $accepted) {
|
|
if ($accepted['sha256'] === $document->sha256) {
|
|
$expectedImageSha = $accepted['original_image_sha256'] ?? null;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$images = $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp']);
|
|
$image = null;
|
|
if ($mode === 'selected' && ! empty($content['image_resource_id'])) {
|
|
$image = (clone $images)->where('r.id', $content['image_resource_id'])->first(['r.id', 'r.title']);
|
|
}
|
|
if ($mode === 'original' && $original) {
|
|
$image = (clone $images)->where(function ($q) use ($expectedImageSha, $originalImageUrl) {
|
|
$q->whereRaw('1=0');
|
|
if ($expectedImageSha) {
|
|
$q->orWhere('d.sha256', $expectedImageSha);
|
|
} elseif ($originalImageUrl) {
|
|
$q->orWhere('r.source_url', $originalImageUrl)->orWhere('d.original_source', $originalImageUrl);
|
|
}
|
|
})->first(['r.id', 'r.title']);
|
|
}
|
|
if ($image) {
|
|
// A public flag alone never licenses missing or changed image bytes.
|
|
$binary = (clone $images)->where('r.id', $image->id)->first(['d.*']);
|
|
try {
|
|
app(VerifiedDocumentBinary::class)->respond($binary);
|
|
} catch (HttpException) {
|
|
$image = null;
|
|
}
|
|
}
|
|
$originalFormat = $original && ($content['preserve_original_html'] ?? false) && $content['body'] === $this->plain($original['content_html']);
|
|
$html = $originalFormat ? app(LegacyContent::class)->sanitize($original['content_html'], true) : null;
|
|
$imageReferences = [];
|
|
if ($original) {
|
|
preg_match_all('~<img\b[^>]*\bsrc\s*=\s*([\'"])(.*?)\1~is', $original['content_html'], $matches);
|
|
foreach ($matches[2] as $source) {
|
|
$url = $this->sourceUrl(html_entity_decode($source, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $original['source_url'] ?? null);
|
|
if ($url) {
|
|
$imageReferences[] = $url;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ['id' => $resource->id, 'title' => $resource->title, 'description' => $resource->description,
|
|
'author' => $document->issuer, 'body' => $content['body'], 'html' => $html,
|
|
'image_url' => $image ? '/documentos/'.$image->id.'/view' : null,
|
|
'image_notice' => $mode === 'selected' && ! $image ? 'A imagem escolhida está indisponível para divulgação; a selecção mantém-se na edição.' : ($mode === 'original' && ! $image && ! empty($original['image_source_url']) ? 'Imagem de origem referenciada, mas sem exemplar público verificado na biblioteca. A referência original mantém-se preservada.' : null),
|
|
'source_url' => $this->sourceUrl($original['source_url'] ?? $resource->source_url),
|
|
'published_at' => $original['published_at'] ?? null, 'modified_at' => $original['modified_at'] ?? null,
|
|
'legacy_id' => $original['legacy_id'] ?? null, 'image_source_url' => $originalImageUrl, 'image_expected_sha256' => $expectedImageSha, 'image_references' => array_values(array_unique($imageReferences)),
|
|
'metadata_version' => $resource->metadata_version, 'private_preview' => $private,
|
|
'original_url' => $private ? '/management/documents/'.$resource->document_id.'/download' : '/documentos/'.$resource->id,
|
|
];
|
|
}
|
|
}
|