*/ /** * Nur die ersten Seiten des ungefilterten Verzeichnisses werden gecacht. */ private const MaxCachedPage = 20; public function build(WebPortalContext $context): array { $portalValues = $context->portalValues; $language = $context->language; $requestedTerm = request()->query('q', ''); $term = is_string($requestedTerm) ? Str::of($requestedTerm)->squish()->toString() : ''; $normalizedTerm = mb_strtolower($term); // Seite exakt wie der Paginator auflösen (verhindert Key/Inhalt-Divergenz). $page = Paginator::resolveCurrentPage(); $build = fn (): array => $this->query( portalValues: $portalValues, language: $language, term: $term, page: $page, ); // Gefilterte Suchen und tiefe Seiten NICHT cachen: sonst kann ein // Crawler/Angreifer über beliebige ?q= / ?page= unbegrenzt Cache- // Einträge (je mit serialisiertem Paginator) erzeugen, die nie // wiederverwendet werden. Der häufige Fall (ungefiltertes Verzeichnis, // erste Seiten) bleibt gecacht. if ($normalizedTerm !== '' || $page > self::MaxCachedPage) { return $build(); } $cacheKey = WebPageCache::key('newsrooms', [implode(',', $portalValues), $language, 'page', $page]); return WebPageCache::remember($cacheKey, $build); } /** * @param list $portalValues * @return array */ private function query(array $portalValues, string $language, string $term, int $page): array { $referenceNow = EditorialClock::reference($portalValues, $language); $recentSince = $referenceNow->copy()->subDays(30); $published = static function (Builder $query) use ($portalValues, $language): void { $query ->forPortalValues($portalValues) ->forLanguage($language) ->visibleInArchive(); }; $activeCompanies = static fn (): Builder => Company::query() ->withoutGlobalScope(PortalScope::class) ->whereIn('portal', $portalValues) ->where('is_active', true) ->whereHas('pressReleases', fn (Builder $query) => $published($query)); $newsrooms = $activeCompanies() ->when($term !== '', fn (Builder $query) => $query->whereLike('name', '%'.$term.'%')) ->withCount([ 'pressReleases as releases_count' => fn (Builder $query) => $published($query), 'pressReleases as recent_count' => function (Builder $query) use ($published, $recentSince): void { $published($query); $query->where('published_at', '>=', $recentSince); }, ]) ->withMax(['pressReleases as latest_published_at' => fn (Builder $query) => $published($query)], 'published_at') ->orderByDesc('recent_count') ->orderByDesc('releases_count') ->orderBy('name') ->paginate(24, ['*'], 'page', $page); if ($term !== '') { $newsrooms->appends(['q' => $term]); } $archiveTotal = PressRelease::query() ->withoutGlobalScope(PortalScope::class) ->tap(fn (Builder $query) => $published($query)) ->count(); return [ 'newsrooms' => $newsrooms, 'term' => $term, 'referenceNow' => $referenceNow, 'totalNewsrooms' => (clone $activeCompanies())->count(), 'activeNewsroomsCount' => $activeCompanies() ->whereHas('pressReleases', function (Builder $query) use ($published, $recentSince): void { $published($query); $query->where('published_at', '>=', $recentSince); }) ->count(), 'archiveTotal' => $archiveTotal, ]; } }