Allow heading tags (h1-h6) in ContentSanitizer so news editor headings render
This commit is contained in:
@@ -26,6 +26,7 @@ use cPad\Plugins\News\Models\NewsArticle;
|
||||
use cPad\Plugins\News\Models\NewsArticleRelation;
|
||||
use cPad\Plugins\News\Models\NewsCategory;
|
||||
use cPad\Plugins\News\Models\NewsTag;
|
||||
use cPad\Plugins\News\Services\NewsArticleService;
|
||||
|
||||
final class NewsService
|
||||
{
|
||||
@@ -39,6 +40,7 @@ final class NewsService
|
||||
public const RELATION_CHALLENGE = 'challenge';
|
||||
public const RELATION_EVENT = 'event';
|
||||
public const RELATION_USER = 'user';
|
||||
public const RELATION_SOURCE = 'source';
|
||||
|
||||
public const RELATION_LABELS = [
|
||||
self::RELATION_GROUP => 'Group',
|
||||
@@ -49,10 +51,15 @@ final class NewsService
|
||||
self::RELATION_CHALLENGE => 'Challenge',
|
||||
self::RELATION_EVENT => 'Event',
|
||||
self::RELATION_USER => 'Profile',
|
||||
self::RELATION_SOURCE => 'Source',
|
||||
];
|
||||
|
||||
private ?bool $artworkStatsViewsColumnExists = null;
|
||||
|
||||
public function __construct(private readonly NewsArticleService $articleService)
|
||||
{
|
||||
}
|
||||
|
||||
public function articleTypeOptions(): array
|
||||
{
|
||||
return \collect(NewsArticle::TYPE_LABELS)
|
||||
@@ -224,12 +231,22 @@ final class NewsService
|
||||
'og_description' => (string) ($article->og_description ?? ''),
|
||||
'og_image' => (string) ($article->og_image ?? ''),
|
||||
'relations' => $article->relatedEntities
|
||||
->map(fn (NewsArticleRelation $relation): array => [
|
||||
'entity_type' => (string) $relation->entity_type,
|
||||
'entity_id' => (int) $relation->entity_id,
|
||||
'context_label' => (string) ($relation->context_label ?? ''),
|
||||
'preview' => $this->resolveEntityPreview((string) $relation->entity_type, (int) $relation->entity_id, $viewer),
|
||||
])
|
||||
->map(function (NewsArticleRelation $relation) use ($viewer): array {
|
||||
$entityType = (string) $relation->entity_type;
|
||||
$externalUrl = $entityType === self::RELATION_SOURCE
|
||||
? (string) ($relation->external_url ?? '')
|
||||
: '';
|
||||
|
||||
return [
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityType === self::RELATION_SOURCE ? '' : (int) $relation->entity_id,
|
||||
'external_url' => $externalUrl,
|
||||
'context_label' => (string) ($relation->context_label ?? ''),
|
||||
'preview' => $entityType === self::RELATION_SOURCE
|
||||
? $this->resolveSourcePreview($externalUrl, (string) ($relation->context_label ?? ''))
|
||||
: $this->resolveEntityPreview($entityType, (int) $relation->entity_id, $viewer),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
@@ -263,6 +280,8 @@ final class NewsService
|
||||
'published_at' => $article->published_at ?? \now(),
|
||||
])->save();
|
||||
|
||||
$this->articleService->createForumThread($article);
|
||||
|
||||
$this->invalidatePublicCache();
|
||||
|
||||
return $article->fresh(['author', 'category', 'tags', 'relatedEntities']);
|
||||
@@ -312,6 +331,7 @@ final class NewsService
|
||||
self::RELATION_CHALLENGE => $this->searchChallenges($query, $viewer),
|
||||
self::RELATION_EVENT => $this->searchEvents($query, $viewer),
|
||||
self::RELATION_USER => $this->searchUsers($query),
|
||||
self::RELATION_SOURCE => [],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
@@ -321,7 +341,15 @@ final class NewsService
|
||||
$article->loadMissing('relatedEntities');
|
||||
|
||||
return $article->relatedEntities
|
||||
->map(fn (NewsArticleRelation $relation): ?array => $this->resolveEntityPreview((string) $relation->entity_type, (int) $relation->entity_id, $viewer, (string) ($relation->context_label ?? '')))
|
||||
->map(function (NewsArticleRelation $relation) use ($viewer): ?array {
|
||||
$entityType = (string) $relation->entity_type;
|
||||
|
||||
if ($entityType === self::RELATION_SOURCE) {
|
||||
return $this->resolveSourcePreview((string) ($relation->external_url ?? ''), (string) ($relation->context_label ?? ''));
|
||||
}
|
||||
|
||||
return $this->resolveEntityPreview($entityType, (int) $relation->entity_id, $viewer, (string) ($relation->context_label ?? ''));
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
@@ -380,6 +408,7 @@ final class NewsService
|
||||
public function invalidatePublicCache(): void
|
||||
{
|
||||
Cache::forever(self::PUBLIC_CACHE_VERSION_KEY, $this->publicCacheVersion() + 1);
|
||||
Cache::forget('news.rss.feed');
|
||||
}
|
||||
|
||||
public function syncRelations(NewsArticle $article, array $relations): void
|
||||
@@ -387,20 +416,32 @@ final class NewsService
|
||||
$normalized = \collect($relations)
|
||||
->map(function (array $relation): ?array {
|
||||
$entityType = trim(Str::lower((string) ($relation['entity_type'] ?? '')));
|
||||
$entityId = (int) ($relation['entity_id'] ?? 0);
|
||||
$externalUrl = $entityType === self::RELATION_SOURCE
|
||||
? $this->normalizeExternalRelationUrl($relation['external_url'] ?? $relation['entity_id'] ?? null)
|
||||
: null;
|
||||
$entityId = $entityType === self::RELATION_SOURCE ? null : (int) ($relation['entity_id'] ?? 0);
|
||||
|
||||
if (! array_key_exists($entityType, self::RELATION_LABELS) || $entityId < 1) {
|
||||
if (! array_key_exists($entityType, self::RELATION_LABELS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entityType === self::RELATION_SOURCE && $externalUrl === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entityType !== self::RELATION_SOURCE && $entityId < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'external_url' => $externalUrl,
|
||||
'context_label' => Str::limit(trim((string) ($relation['context_label'] ?? '')), 120, ''),
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->unique(fn (array $relation): string => $relation['entity_type'] . ':' . $relation['entity_id'])
|
||||
->unique(fn (array $relation): string => $relation['entity_type'] . ':' . ($relation['entity_type'] === self::RELATION_SOURCE ? ($relation['external_url'] ?? '') : $relation['entity_id']))
|
||||
->values();
|
||||
|
||||
$article->relatedEntities()->delete();
|
||||
@@ -409,6 +450,7 @@ final class NewsService
|
||||
$article->relatedEntities()->create([
|
||||
'entity_type' => $relation['entity_type'],
|
||||
'entity_id' => $relation['entity_id'],
|
||||
'external_url' => $relation['external_url'],
|
||||
'context_label' => $relation['context_label'] !== '' ? $relation['context_label'] : null,
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
@@ -808,6 +850,34 @@ final class NewsService
|
||||
};
|
||||
}
|
||||
|
||||
private function resolveSourcePreview(string $externalUrl, string $contextLabel): ?array
|
||||
{
|
||||
$normalizedUrl = $this->normalizeExternalRelationUrl($externalUrl);
|
||||
|
||||
if ($normalizedUrl === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = \parse_url($normalizedUrl, PHP_URL_HOST);
|
||||
$host = \is_string($host) ? preg_replace('/^www\./i', '', $host) : null;
|
||||
|
||||
return [
|
||||
'id' => $normalizedUrl,
|
||||
'entity_type' => self::RELATION_SOURCE,
|
||||
'entity_label' => self::RELATION_LABELS[self::RELATION_SOURCE],
|
||||
'title' => $host ?: 'External source',
|
||||
'subtitle' => 'Reference link',
|
||||
'description' => Str::limit($normalizedUrl, 140),
|
||||
'url' => $normalizedUrl,
|
||||
'image' => null,
|
||||
'avatar' => null,
|
||||
'context_label' => $contextLabel !== '' ? $contextLabel : 'Source link',
|
||||
'meta' => array_values(array_filter([
|
||||
$host,
|
||||
])),
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveGroupPreview(int $entityId, ?User $viewer, string $contextLabel): ?array
|
||||
{
|
||||
$group = Group::query()->with('owner')->find($entityId);
|
||||
@@ -1017,4 +1087,23 @@ final class NewsService
|
||||
'meta' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeExternalRelationUrl(mixed $value): ?string
|
||||
{
|
||||
$url = trim((string) ($value ?? ''));
|
||||
|
||||
if ($url === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/i', $url, $matches) === 1) {
|
||||
$url = trim((string) ($matches[1] ?? ''));
|
||||
}
|
||||
|
||||
if ($url === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Str::limit($url, 2048, '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user