TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:39 +02:00
commit e54ab24745
68 changed files with 3683 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\Canonical;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Domain\Page;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Type\DocType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Utility\CanonicalizationUtility;
use TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent;
use TYPO3\CMS\Seo\Exception\CanonicalGenerationDisabledException;
/**
* Class to add the canonical tag to the page
*
* @internal this class is not part of TYPO3's Core API.
*/
#[Autoconfigure(public: true)]
readonly class CanonicalGenerator
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private PageRenderer $pageRenderer,
private RecordFactory $recordFactory,
private PageRepository $pageRepository,
) {}
public function generate(array $params): string
{
/** @var ServerRequestInterface $request */
$request = $params['request'];
$pageRecord = $request->getAttribute('frontend.page.information')->getPageRecord();
$canonicalGenerationDisabledException = null;
$href = '';
$typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
try {
if ($typoScriptConfigArray['disableCanonical'] ?? false) {
throw new CanonicalGenerationDisabledException('Generation of the canonical tag is disabled via TypoScript "disableCanonical"', 1706104146);
}
if ((int)$pageRecord['no_index'] === 1) {
throw new CanonicalGenerationDisabledException('Generation of the canonical is disabled due to "no_index" being set active in the page properties', 1706104147);
}
// 1) Check if page has canonical URL set
$href = $this->checkForCanonicalLink($request);
if ($href === '') {
// 2) Check if page show content from other page
$href = $this->checkContentFromPid($request);
}
if ($href === '') {
// 3) Fallback, create canonical URL
$href = $this->checkDefaultCanonical($request);
}
} catch (CanonicalGenerationDisabledException $canonicalGenerationDisabledException) {
} finally {
/** @var Page $page */
$page = $this->recordFactory->createFromDatabaseRow('pages', $pageRecord);
$event = $this->eventDispatcher->dispatch(
new ModifyUrlForCanonicalTagEvent($request, $page, $href, $canonicalGenerationDisabledException)
);
$href = $event->getUrl();
}
if ($href !== '') {
$docType = DocType::createFromConfigurationKey($typoScriptConfigArray['doctype'] ?? '');
$canonical = '<link ' . GeneralUtility::implodeAttributes([
'rel' => 'canonical',
'href' => $href,
], true) . ($docType->isXmlCompliant() ? '/' : '') . '>' . LF;
$this->pageRenderer->addHeaderData($canonical);
return $canonical;
}
return '';
}
protected function checkForCanonicalLink(ServerRequestInterface $request): string
{
$pageRecord = $request->getAttribute('frontend.page.information')->getPageRecord();
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$cObj->start($pageRecord, 'pages');
if (!empty($pageRecord['canonical_link'])) {
return $cObj->createUrl([
'parameter' => $pageRecord['canonical_link'],
'forceAbsoluteUrl' => true,
]);
}
return '';
}
protected function checkContentFromPid(ServerRequestInterface $request): string
{
$pageInformation = $request->getAttribute('frontend.page.information');
$id = $pageInformation->getId();
$contentPid = $pageInformation->getContentFromPid();
if ($id !== $contentPid) {
$targetPid = $contentPid;
if ($targetPid > 0) {
$targetPageRecord = $this->pageRepository->getPage($contentPid, true);
if (!empty($targetPageRecord['canonical_link'])) {
$targetPid = $targetPageRecord['canonical_link'];
}
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$cObj->start($request->getAttribute('frontend.page.information')->getPageRecord(), 'pages');
return $cObj->createUrl([
'parameter' => $targetPid,
'forceAbsoluteUrl' => true,
]);
}
}
return '';
}
protected function checkDefaultCanonical(ServerRequestInterface $request): string
{
$pageInformation = $request->getAttribute('frontend.page.information');
$id = $pageInformation->getId();
// We should only create a canonical link to the target, if the target is within a valid site root
$inSiteRoot = $this->isPageWithinSiteRoot($id);
if (!$inSiteRoot) {
return '';
}
// Temporarily remove current mount point information as we want to have the
// URL of the target page and not of the page within the mount point if the
// current page is a mount point.
$pageInformation = clone $pageInformation;
$pageInformation->setMountPoint('');
$request = $request->withAttribute('frontend.page.information', $pageInformation);
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$cObj->start($pageInformation->getPageRecord(), 'pages');
return $cObj->createUrl([
'parameter' => $id . ',' . $request->getAttribute('routing')->getPageType(),
'forceAbsoluteUrl' => true,
'addQueryString' => true,
'addQueryString.' => [
'exclude' => implode(
',',
CanonicalizationUtility::getParamsToExcludeForCanonicalizedUrl(
$id,
(array)$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'],
$request
)
),
],
]);
}
protected function isPageWithinSiteRoot(int $id): bool
{
$rootline = GeneralUtility::makeInstance(RootlineUtility::class, $id)->get();
foreach ($rootline as $page) {
if ($page['is_siteroot']) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\Page;
use TYPO3\CMS\Seo\Exception\CanonicalGenerationDisabledException;
/**
* PSR-14 event to alter (or empty) a canonical URL for the href="" attribute of a canonical URL.
*/
final class ModifyUrlForCanonicalTagEvent
{
public function __construct(
private readonly ServerRequestInterface $request,
private readonly Page $page,
private string $url,
private readonly ?CanonicalGenerationDisabledException $canonicalGenerationDisabledException
) {}
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): void
{
$this->url = $url;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getPage(): Page
{
return $this->page;
}
public function getCanonicalGenerationDisabledException(): ?CanonicalGenerationDisabledException
{
return $this->canonicalGenerationDisabledException;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\Exception;
use TYPO3\CMS\Core\Exception;
/**
* Thrown if generation of the canonical is disabled
*/
class CanonicalGenerationDisabledException extends Exception {}
+121
View File
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\HrefLang;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
/**
* Class to add the hreflang tags to the page
*
* @internal
*/
class HrefLangGenerator
{
public function __construct(
protected ContentObjectRenderer $cObj,
protected LanguageMenuProcessor $languageMenuProcessor,
) {}
#[AsEventListener('typo3-seo/hreflangGenerator')]
public function __invoke(ModifyHrefLangTagsEvent $event): void
{
$request = $event->getRequest();
$pageInformation = $request->getAttribute('frontend.page.information');
$pageRecord = $pageInformation->getPageRecord();
if ((int)$pageRecord['no_index'] === 1) {
return;
}
$this->cObj->setRequest($event->getRequest());
$languages = $this->languageMenuProcessor->process($this->cObj, [], [], []);
$site = $request->getAttribute('site');
$siteLanguage = $request->getAttribute('language', $site->getDefaultLanguage());
$pageId = $pageInformation->getId();
$hrefLangs = $event->getHrefLangs();
foreach ($languages['languagemenu'] as $language) {
if (!empty($language['link']) && $language['hreflang']) {
if ($language['languageId'] === 0) {
// No need to fetch default language
$page = ($pageRecord['_TRANSLATION_SOURCE'] ?? null)?->toArray(true) ?? $pageRecord;
} elseif ($language['languageId'] === ($pageRecord['_REQUESTED_OVERLAY_LANGUAGE'] ?? false)) {
// No need to fetch current language
$page = $pageRecord;
} else {
$page = $this->getTranslatedPageRecord($pageId, $language['languageId'], $site);
}
// do not set hreflang if a page is not translated explicitly
if (empty($page)) {
continue;
}
// do not set hreflang when canonical is set explicitly
if (!empty($page['canonical_link'])) {
continue;
}
$href = $this->getAbsoluteUrl($language['link'], $siteLanguage);
$hrefLangs[$language['hreflang']] = $href;
}
}
if (count($hrefLangs) > 1) {
if (array_key_exists($languages['languagemenu'][0]['hreflang'], $hrefLangs)) {
$hrefLangs['x-default'] = $hrefLangs[$languages['languagemenu'][0]['hreflang']];
}
}
$event->setHrefLangs($hrefLangs);
}
protected function getAbsoluteUrl(string $url, SiteLanguage $siteLanguage): string
{
$uri = new Uri($url);
if (empty($uri->getHost())) {
$url = $siteLanguage->getBase()->withPath($uri->getPath());
if ($uri->getQuery()) {
$url = $url->withQuery($uri->getQuery());
}
}
return (string)$url;
}
protected function getTranslatedPageRecord(int $pageId, int $languageId, Site $site): array
{
$targetSiteLanguage = $site->getLanguageById($languageId);
$languageAspect = LanguageAspectFactory::createFromSiteLanguage($targetSiteLanguage);
$context = clone GeneralUtility::makeInstance(Context::class);
$context->setAspect('language', $languageAspect);
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
$pageRecord = $pageRepository->getPage($pageId);
// Overlay was requested but did not apply
if ($languageId > 0 && !isset($pageRecord['_LOCALIZED_UID'])) {
return [];
}
return $pageRecord;
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\MetaTag;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\Resource\FileCollector;
/**
* Class to add the metatags for the SEO fields in core
*
* @internal
*/
#[Autoconfigure(public: true)]
readonly class MetaTagGenerator
{
public function __construct(
protected MetaTagManagerRegistry $metaTagManagerRegistry,
protected ImageService $imageService
) {}
/**
* Generate the meta tags that can be set in backend and add them to frontend by using the MetaTag API
*/
public function generate(array $params)
{
$twitterCardTagRequired = false;
/** @var ServerRequestInterface $request */
$request = $params['request'];
$pageRecord = $request->getAttribute('frontend.page.information')->getPageRecord();
if (!empty($pageRecord['description'])) {
$manager = $this->metaTagManagerRegistry->getManagerForProperty('description');
$manager->addProperty('description', $pageRecord['description']);
}
if (!empty($pageRecord['og_title'])) {
$twitterCardTagRequired = true;
$manager = $this->metaTagManagerRegistry->getManagerForProperty('og:title');
$manager->addProperty('og:title', $pageRecord['og_title']);
}
if (!empty($pageRecord['og_description'])) {
$twitterCardTagRequired = true;
$manager = $this->metaTagManagerRegistry->getManagerForProperty('og:description');
$manager->addProperty('og:description', $pageRecord['og_description']);
}
if (!empty($pageRecord['og_image'])) {
$fileCollector = GeneralUtility::makeInstance(FileCollector::class);
$fileCollector->addFilesFromRelation('pages', 'og_image', $pageRecord);
$manager = $this->metaTagManagerRegistry->getManagerForProperty('og:image');
$ogImages = $this->generateSocialImages($fileCollector->getFiles());
foreach ($ogImages as $ogImage) {
$twitterCardTagRequired = true;
$subProperties = [];
$subProperties['url'] = $ogImage['url'];
$subProperties['width'] = $ogImage['width'];
$subProperties['height'] = $ogImage['height'];
if (!empty($ogImage['alternative'])) {
$subProperties['alt'] = $ogImage['alternative'];
}
$manager->addProperty(
'og:image',
$ogImage['url'],
$subProperties
);
}
}
if (!empty($pageRecord['twitter_title'])) {
$twitterCardTagRequired = true;
$manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:title');
$manager->addProperty('twitter:title', $pageRecord['twitter_title']);
}
if (!empty($pageRecord['twitter_description'])) {
$twitterCardTagRequired = true;
$manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:description');
$manager->addProperty('twitter:description', $pageRecord['twitter_description']);
}
if (!empty($pageRecord['twitter_image'])) {
$fileCollector = GeneralUtility::makeInstance(FileCollector::class);
$fileCollector->addFilesFromRelation('pages', 'twitter_image', $pageRecord);
$manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:image');
$twitterImages = $this->generateSocialImages($fileCollector->getFiles());
foreach ($twitterImages as $twitterImage) {
$twitterCardTagRequired = true;
$subProperties = [];
if (!empty($twitterImage['alternative'])) {
$subProperties['alt'] = $twitterImage['alternative'];
}
$manager->addProperty(
'twitter:image',
$twitterImage['url'],
$subProperties
);
}
}
$twitterCard = $pageRecord['twitter_card'] ?: ($twitterCardTagRequired ? 'summary' : '');
if (!empty($twitterCard)) {
$manager = $this->metaTagManagerRegistry->getManagerForProperty('twitter:card');
$manager->addProperty('twitter:card', $twitterCard);
}
$noIndex = ($pageRecord['no_index']) ? 'noindex' : 'index';
$noFollow = ($pageRecord['no_follow']) ? 'nofollow' : 'follow';
if ($noIndex === 'noindex' || $noFollow === 'nofollow') {
$manager = $this->metaTagManagerRegistry->getManagerForProperty('robots');
$manager->addProperty('robots', implode(',', [$noIndex, $noFollow]));
}
}
/**
* @param list<FileReference> $fileReferences
*/
protected function generateSocialImages(array $fileReferences): array
{
$socialImages = [];
foreach ($fileReferences as $fileReference) {
$arguments = $fileReference->getProperties();
$image = $this->processSocialImage($fileReference);
$socialImages[] = [
'url' => $this->imageService->getImageUri($image, true),
'width' => floor((float)$image->getProperty('width')),
'height' => floor((float)$image->getProperty('height')),
'alternative' => $arguments['alternative'],
];
}
return $socialImages;
}
protected function processSocialImage(FileReference $fileReference): FileInterface
{
$arguments = $fileReference->getProperties();
$cropVariantCollection = CropVariantCollection::create((string)($arguments['crop'] ?? ''));
$cropVariantName = ($arguments['cropVariant'] ?? false) ?: 'social';
$cropArea = $cropVariantCollection->getCropArea($cropVariantName);
$crop = $cropArea->makeAbsoluteBasedOnFile($fileReference);
$processingConfiguration = [
'crop' => $crop,
'maxWidth' => 2000,
];
// The image needs to be processed if:
// - the image width is greater than the defined maximum width, or
// - there is a cropping other than the full image (starts at 0,0 and has a width and height of 100%) defined
$needsProcessing = $fileReference->getProperty('width') > $processingConfiguration['maxWidth']
|| !$cropArea->isEmpty();
if (!$needsProcessing) {
return $fileReference->getOriginalFile();
}
return $fileReference->getOriginalFile()->process(
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
$processingConfiguration
);
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\MetaTag;
use TYPO3\CMS\Core\MetaTag\AbstractMetaTagManager;
/**
* @internal this class is not part of TYPO3's Core API.
*/
class OpenGraphMetaTagManager extends AbstractMetaTagManager
{
/**
* The default attribute that defines the name of the property
*
* This creates tags like <meta property="" /> by default
*
* @var string
*/
protected $defaultNameAttribute = 'property';
/**
* Array of properties that can be handled by this manager
*
* @var array
*/
protected $handledProperties = [
'og:type' => [],
'og:title' => [],
'og:description' => [],
'og:site_name' => [],
'og:url' => [],
'og:audio' => [],
'og:video' => [],
'og:determiner' => [],
'og:locale' => [
'allowedSubProperties' => [
'alternate' => [
'allowMultipleOccurrences' => true,
],
],
],
'og:image' => [
'allowMultipleOccurrences' => true,
'allowedSubProperties' => [
'url' => [],
'secure_url' => [],
'type' => [],
'width' => [],
'height' => [],
'alt' => [],
],
],
];
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\MetaTag;
use TYPO3\CMS\Core\MetaTag\AbstractMetaTagManager;
/**
* @internal this class is not part of TYPO3's Core API.
*/
class TwitterCardMetaTagManager extends AbstractMetaTagManager
{
/**
* Array of properties that can be handled by this manager
*
* @var array
*/
protected $handledProperties = [
'twitter:card' => [],
'twitter:site' => [
'allowedSubProperties' => [
'id' => [],
],
],
'twitter:creator' => [
'allowedSubProperties' => [
'id' => [],
],
],
'twitter:description' => [],
'twitter:title' => [],
'twitter:image' => [
'allowedSubProperties' => [
'alt' => [],
],
],
'twitter:player' => [
'allowedSubProperties' => [
'width' => [],
'height' => [],
'stream' => [],
],
],
'twitter:app' => [
'allowedSubProperties' => [
'name:iphone' => [],
'id:iphone' => [],
'url:iphone' => [],
'name:ipad' => [],
'id:ipad' => [],
'url:ipad' => [],
'name:googleplay' => [],
'id:googleplay' => [],
'url:googleplay' => [],
],
],
];
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\PageTitle;
use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;
/**
* This class will take care of the seo title that can be set in the backend
* @internal this class is not part of TYPO3's Core API.
*/
class SeoTitlePageTitleProvider extends AbstractPageTitleProvider
{
public function getTitle(): string
{
$pageInformation = $this->request->getAttribute('frontend.page.information');
return (string)($pageInformation->getPageRecord()['seo_title'] ?? '');
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\Widgets;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Dashboard\Widgets\RequestAwareWidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Seo\Widgets\Provider\PagesWithoutDescriptionDataProvider;
/**
* @internal
*/
final class PagesWithoutDescriptionWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly PagesWithoutDescriptionDataProvider $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly array $options,
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request, ['typo3/cms-dashboard', 'typo3/cms-seo']);
$view->assignMultiple([
'pages' => $this->dataProvider->getPages(),
'options' => $this->getOptions(),
'configuration' => $this->configuration,
]);
return $view->render('Widget/PagesWithoutDescription');
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\Widgets\Provider;
use Doctrine\DBAL\Result;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
/**
* @internal
*/
final readonly class PagesWithoutDescriptionDataProvider
{
public function __construct(
private SiteFinder $siteFinder,
private ConnectionPool $connectionPool,
private array $excludedDoktypes,
private int $limit
) {}
public function getPages(): array
{
$backendUser = $this->getBackendUser();
$items = [];
if (!$backendUser->check('tables_modify', 'pages')) {
// Early return in case user is not allowed to modify pages at all
return $items;
}
$rowCount = 0;
$pagesResult = $this->getPotentialPages();
while ($row = $pagesResult->fetchAssociative()) {
if (!$backendUser->doesUserHaveAccess($row, Permission::PAGE_EDIT)) {
continue;
}
BackendUtility::workspaceOL('pages', $row, $backendUser->workspace);
$pageId = $row['l10n_parent'] ?: $row['uid'];
try {
$site = $this->siteFinder->getSiteByPageId($pageId);
// make sure the language of the row actually exists in the site
$site->getLanguageById($row['language_tag']);
} catch (SiteNotFoundException|\InvalidArgumentException) {
continue;
}
$router = $site->getRouter();
$row['frontendUrl'] = (string)$router->generateUri($pageId, ['_language' => $row['language_tag']]);
$items[] = $row;
$rowCount++;
if ($rowCount >= $this->limit) {
return $items;
}
}
return $items;
}
/**
* Fetches potential candidates for the list from the database.
* Doktypes that do not require a meta description (such as directories and links) are ignored.
* Pages with noindex or a canonical are also ignored for this reason.
* Language versions are considered individually.
* Workspace versions are considered for the workspace the user is in.
*/
private function getPotentialPages(): Result
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->add(new WorkspaceRestriction($this->getBackendUser()->workspace));
return $queryBuilder
->select('uid', 'pid', 'title', 'slug', 'sys_language_uid', 'l10n_parent', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
->from('pages')
->where(
$queryBuilder->expr()->notIn('doktype', $this->excludedDoktypes),
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq('no_index', $queryBuilder->createNamedParameter(0)),
$queryBuilder->expr()->eq('canonical_link', $queryBuilder->createNamedParameter('')),
),
$queryBuilder->expr()->or(
$queryBuilder->expr()->eq('description', $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->isNull('description')
),
)
->orderBy('tstamp', 'DESC')
->executeQuery();
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Base class for XmlSitemapProviders to extend
*/
abstract class AbstractXmlSitemapDataProvider implements XmlSitemapDataProviderInterface
{
protected string $key;
protected array $items = [];
protected array $config = [];
protected ContentObjectRenderer $cObj;
protected int $numberOfItemsPerPage = 1000;
protected ServerRequestInterface $request;
public function __construct(ServerRequestInterface $request, string $key, array $config = [], ?ContentObjectRenderer $cObj = null)
{
$this->key = $key;
$this->config = $config;
$this->request = $request;
$this->cObj = $cObj ?? GeneralUtility::makeInstance(ContentObjectRenderer::class);
$this->cObj->setRequest($request);
}
public function getKey(): string
{
return $this->key;
}
public function getNumberOfPages(): int
{
return (int)ceil(count($this->items) / $this->numberOfItemsPerPage);
}
public function getLastModified(): int
{
$lastMod = 0;
foreach ($this->items as $item) {
if ((int)($item['lastMod'] ?? 0) > $lastMod) {
$lastMod = (int)$item['lastMod'];
}
}
return $lastMod;
}
protected function defineUrl(array $data): array
{
return $data;
}
public function getItems(): array
{
$pageNumber = (int)($this->request->getQueryParams()['tx_seo']['page'] ?? 0);
$page = $pageNumber > 0 ? $pageNumber : 0;
$items = array_slice(
$this->items,
$page * $this->numberOfItemsPerPage,
$this->numberOfItemsPerPage
);
return array_map([$this, 'defineUrl'], $items);
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap\Exception;
use TYPO3\CMS\Core\Resource\Exception;
class InvalidConfigurationException extends Exception {}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap\Exception;
use TYPO3\CMS\Core\Resource\Exception;
class MissingConfigurationException extends Exception {}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Class to generate a XML sitemap for pages
* @internal this class is not part of TYPO3's Core API.
*/
class PagesXmlSitemapDataProvider extends AbstractXmlSitemapDataProvider
{
public function __construct(ServerRequestInterface $request, string $key, array $config = [], ?ContentObjectRenderer $cObj = null)
{
parent::__construct($request, $key, $config, $cObj);
$this->generateItems();
}
protected function generateItems(): void
{
$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$pages = $pageRepository->getPagesOverlay($this->getPages());
$languageAspect = $this->getCurrentLanguageAspect();
foreach ($pages as $page) {
if (!$pageRepository->isPageSuitableForLanguage($page, $languageAspect)) {
continue;
}
$this->items[] = $page + [
'lastMod' => (int)($page['SYS_LASTCHANGED'] ?: $page['tstamp']),
'changefreq' => $page['sitemap_changefreq'],
'priority' => (float)$page['sitemap_priority'],
];
}
}
protected function getPages(): array
{
if (!empty($this->config['rootPage'])) {
$rootPageId = (int)$this->config['rootPage'];
} else {
$site = $this->request->getAttribute('site');
$rootPageId = $site->getRootPageId();
}
$excludePagesRecursive = GeneralUtility::intExplode(',', (string)($this->config['excludePagesRecursive'] ?? ''), true);
$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$pageIds = $pageRepository->getDescendantPageIdsRecursive($rootPageId, 99, 0, $excludePagesRecursive);
$pageIds = array_merge([$rootPageId], $pageIds);
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$constraints = [
$queryBuilder->expr()->in('uid', $pageIds),
];
if (!empty($this->config['additionalWhere'])) {
$constraints[] = QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), QueryHelper::stripLogicalOperatorPrefix($this->config['additionalWhere']));
}
if (!empty($this->config['excludedDoktypes'])) {
$excludedDoktypes = GeneralUtility::intExplode(',', (string)$this->config['excludedDoktypes']);
if (!empty($excludedDoktypes)) {
$constraints[] = $queryBuilder->expr()->notIn('doktype', implode(',', $excludedDoktypes));
}
}
$pages = $queryBuilder->select('*')
->from('pages')
->where(...$constraints)
->orderBy('uid', 'ASC')
->executeQuery()
->fetchAllAssociative();
return $pages;
}
protected function getCurrentLanguageAspect(): LanguageAspect
{
return GeneralUtility::makeInstance(Context::class)->getAspect('language');
}
protected function defineUrl(array $data): array
{
$typoLinkConfig = [
'page' => GeneralUtility::makeInstance(RecordFactory::class)->createFromDatabaseRow('pages', $data),
'parameter' => $data['uid'],
'forceAbsoluteUrl' => 1,
];
$data['loc'] = $this->cObj->createUrl($typoLinkConfig);
return $data;
}
}
@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Schema\Capability\LanguageAwareSchemaCapability;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Seo\XmlSitemap\Exception\MissingConfigurationException;
/**
* XmlSiteDataProvider will provide information for the XML sitemap for a specific database table
* @internal this class is not part of TYPO3's Core API.
*/
class RecordsXmlSitemapDataProvider extends AbstractXmlSitemapDataProvider
{
private TcaSchemaFactory $tcaSchemaFactory;
public function __construct(ServerRequestInterface $request, string $key, array $config = [], ?ContentObjectRenderer $cObj = null)
{
parent::__construct($request, $key, $config, $cObj);
$this->tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
$this->generateItems();
}
/**
* @throws MissingConfigurationException
*/
public function generateItems(): void
{
$table = $this->config['table'];
if (!$this->tcaSchemaFactory->has($table)) {
throw new MissingConfigurationException(
'No configuration found for sitemap ' . $this->getKey(),
1535576053
);
}
$schema = $this->tcaSchemaFactory->get($table);
$pids = !empty($this->config['pid']) ? GeneralUtility::intExplode(',', (string)$this->config['pid']) : [];
$lastModifiedField = $this->config['lastModifiedField'] ?? 'tstamp';
$sortField = $this->config['sortField'] ?? 'sorting';
$changeFreqField = $schema->hasField($this->config['changeFreqField'] ?? '') ? $this->config['changeFreqField'] : '';
$priorityField = $schema->hasField($this->config['priorityField'] ?? '') ? $this->config['priorityField'] : '';
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($table);
$constraints = [];
if ($schema->isLanguageAware()) {
/** @var LanguageAwareSchemaCapability $languageCapability */
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$constraints[] = $queryBuilder->expr()->in(
$languageCapability->getLanguageField()->getName(),
[
-1, // All languages
$this->getLanguageId(), // Current language
]
);
}
if (!empty($pids)) {
$recursiveLevel = isset($this->config['recursive']) ? (int)$this->config['recursive'] : 0;
$pids = GeneralUtility::makeInstance(PageRepository::class)->getPageIdsRecursive($pids, $recursiveLevel);
$constraints[] = $queryBuilder->expr()->in('pid', $pids);
}
if (!empty($this->config['additionalWhere'])) {
$constraints[] = QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), QueryHelper::stripLogicalOperatorPrefix($this->config['additionalWhere']));
}
$queryBuilder->getRestrictions()->add(
GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getCurrentWorkspaceAspect()->getId())
);
$queryBuilder->select('*')
->from($table);
if (!empty($constraints)) {
$queryBuilder->where(
...$constraints
);
}
$rows = $queryBuilder->orderBy($sortField)
->executeQuery()
->fetchAllAssociative();
foreach ($rows as $row) {
$item = [
'data' => $row,
'lastMod' => (int)$row[$lastModifiedField],
];
if (!empty($changeFreqField)) {
$item['changefreq'] = $row[$changeFreqField];
}
$item['priority'] = !empty($priorityField) ? $row[$priorityField] : 0.5;
$this->items[] = $item;
}
}
protected function defineUrl(array $data): array
{
$pageId = $this->request->getAttribute('frontend.page.information')->getId();
$pageId = $this->config['url']['pageId'] ?? $pageId;
$additionalParams = $this->getUrlFieldParameterMap($data['data']);
$additionalParams = $this->getUrlAdditionalParams($additionalParams);
$data['loc'] = $this->cObj->createUrl([
'parameter' => $pageId,
'queryParameters' => $additionalParams,
'forceAbsoluteUrl' => 1,
]);
return $data;
}
protected function getUrlFieldParameterMap(array $data): array
{
$additionalParams = [];
if (!empty($this->config['url']['fieldToParameterMap'])
&& is_array($this->config['url']['fieldToParameterMap'])) {
foreach ($this->config['url']['fieldToParameterMap'] as $field => $urlPart) {
$paramValue = $data[$field];
parse_str($urlPart . '=' . urlencode((string)$paramValue), $nested);
$additionalParams = array_replace_recursive($additionalParams, $nested);
}
}
return $additionalParams;
}
protected function getUrlAdditionalParams(array $additionalParams): array
{
if (!empty($this->config['url']['additionalGetParameters'])
&& is_array($this->config['url']['additionalGetParameters'])) {
$additionalParams = array_replace_recursive($additionalParams, $this->config['url']['additionalGetParameters']);
}
return $additionalParams;
}
protected function getLanguageId(): int
{
$context = GeneralUtility::makeInstance(Context::class);
return (int)$context->getPropertyFromAspect('language', 'id');
}
protected function getCurrentWorkspaceAspect(): WorkspaceAspect
{
return GeneralUtility::makeInstance(Context::class)->getAspect('workspace');
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Interface for XmlSitemapDataProviders containing the methods that are called by the XmlSitemapRenderer
*/
interface XmlSitemapDataProviderInterface
{
public function __construct(ServerRequestInterface $request, string $name, array $config = [], ?ContentObjectRenderer $cObj = null);
public function getKey(): string;
public function getItems(): array;
public function getLastModified(): int;
public function getNumberOfPages(): int;
}
+201
View File
@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Seo\XmlSitemap;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashValue;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyRegistry;
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolvePublicResourceException;
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolveSystemResourceException;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceDoesNotExistException;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\SystemResourceInterface;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Core\View\ViewInterface;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Seo\XmlSitemap\Exception\InvalidConfigurationException;
/**
* Class to render the XML Sitemap to be used as a UserFunction.
*
* @internal this class is not part of TYPO3's Core API.
*/
#[Autoconfigure(public: true)]
final readonly class XmlSitemapRenderer
{
public function __construct(
private TypoScriptService $typoScriptService,
private ErrorController $errorController,
private ViewFactoryInterface $viewFactory,
private PolicyRegistry $policyRegistry,
private SystemResourceFactory $resourceFactory,
private SystemResourcePublisherInterface $resourcePublisher,
) {}
/**
* @param string $_ unused, but needed as this is called via userfunc and passes a string as first parameter
* @param array $typoScriptConfiguration TypoScript configuration specified in USER Content Object
* @throws InvalidConfigurationException
*/
#[AsAllowedCallable]
public function render(string $_, array $typoScriptConfiguration, ServerRequestInterface $request): string
{
$settingsTree = $request->getAttribute('frontend.typoscript')->getSetupTree()->getChildByName('plugin')->getChildByName('tx_seo');
$configurationArrayWithoutDots = $this->typoScriptService->convertTypoScriptArrayToPlainArray($settingsTree->toArray());
$viewConfiguration = $configurationArrayWithoutDots['view'] ?? [];
$viewFactoryData = new ViewFactoryData(
templateRootPaths: $viewConfiguration['templateRootPaths'] ?? [],
partialRootPaths: $viewConfiguration['partialRootPaths'] ?? [],
layoutRootPaths: $viewConfiguration['layoutRootPaths'] ?? [],
request: $request,
format: 'xml',
);
$view = $this->viewFactory->create($viewFactoryData);
$sitemapType = $typoScriptConfiguration['sitemapType'] ?? 'xmlSitemap';
$view->assign('type', $request->getAttribute('routing')->getPageType());
$view->assign('sitemapType', $sitemapType);
$configConfiguration = $configurationArrayWithoutDots['config'] ?? [];
if (!empty($sitemapName = ($request->getQueryParams()['tx_seo']['sitemap'] ?? null))) {
$xslResource = $this->getXslResource($configConfiguration, $sitemapType, $sitemapName);
$this->applyDynamicContentSecurityPolicy($xslResource);
$view->assign('xslFile', (string)$this->resourcePublisher->generateUri($xslResource, $request));
return $this->renderSitemap($request, $view, $configConfiguration, $sitemapType, $sitemapName);
}
$xslResource = $this->getXslResource($configConfiguration, $sitemapType);
$this->applyDynamicContentSecurityPolicy($xslResource);
$view->assign('xslFile', (string)$this->resourcePublisher->generateUri($xslResource, $request));
return $this->renderIndex($request, $view, $configConfiguration, $sitemapType);
}
private function renderIndex(ServerRequestInterface $request, ViewInterface $view, array $configConfiguration, string $sitemapType): string
{
$sitemaps = [];
foreach ($configConfiguration[$sitemapType]['sitemaps'] as $sitemapName => $sitemapConfig) {
$sitemapProvider = $sitemapConfig['provider'] ?? null;
if (is_string($sitemapName)
&& is_string($sitemapProvider)
&& class_exists($sitemapProvider)
&& is_subclass_of($sitemapProvider, XmlSitemapDataProviderInterface::class)
) {
/** @var XmlSitemapDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance($sitemapProvider, $request, $sitemapName, $sitemapConfig['config'] ?? []);
$pages = $provider->getNumberOfPages();
for ($page = 0; $page < $pages; $page++) {
$sitemaps[] = [
'key' => $sitemapName,
'page' => $page,
'lastMod' => $provider->getLastModified(),
];
}
}
}
$view->assign('sitemaps', $sitemaps);
return $view->render('Index');
}
private function renderSitemap(ServerRequestInterface $request, ViewInterface $view, array $configConfiguration, string $sitemapType, string $sitemapName): string
{
$sitemapConfig = $configConfiguration[$sitemapType]['sitemaps'][$sitemapName] ?? null;
if ($sitemapConfig) {
$sitemapProvider = $sitemapConfig['provider'] ?? null;
if (is_string($sitemapProvider)
&& class_exists($sitemapProvider)
&& is_subclass_of($sitemapProvider, XmlSitemapDataProviderInterface::class)
) {
/** @var XmlSitemapDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance($sitemapProvider, $request, $sitemapName, $sitemapConfig['config'] ?? []);
$items = $provider->getItems();
$view->assign('items', $items);
$template = $sitemapConfig['config']['template'] ?? $sitemapConfig['template'] ?? 'Sitemap';
return $view->render($template);
}
throw new InvalidConfigurationException('No valid provider set for ' . $sitemapName, 1535578522);
}
throw new PropagateResponseException(
$this->errorController->pageNotFoundAction(
$request,
'No valid configuration found for sitemap ' . $sitemapName
),
1535578569
);
}
/**
* @throws CanNotResolvePublicResourceException
* @throws CanNotResolveSystemResourceException
*/
private function getXslResource(array $configConfiguration, string $sitemapType, ?string $sitemapName = null): PublicResourceInterface&SystemResourceInterface
{
$resourceIdentifier = $configConfiguration[$sitemapType]['sitemaps'][$sitemapName ?? '']['config']['xslFile']
?? $configConfiguration[$sitemapType]['sitemaps']['xslFile']
?? $configConfiguration['xslFile']
?? 'EXT:seo/Resources/Public/CSS/Sitemap.xsl';
$xslResource = $this->resourceFactory->createPublicResource($resourceIdentifier);
if (!$xslResource instanceof SystemResourceInterface) {
throw new \InvalidArgumentException('Can not resolve xslFile "%s" to a system resource', 1761032332);
}
return $xslResource;
}
/**
* Applies `Content-Security-Policy` mutations for `unsafe-hashes` for XSLT styles.
* This is done dynamically, since XSLT styles might change some day...
*
* The expected hash for the default XSLT styles is `sha256-d0ax6zoVJBeBpy4l3O2FJ6Y1L4SalCWw2x62uoJH15k=`.
*/
private function applyDynamicContentSecurityPolicy(SystemResourceInterface $xslResource): void
{
try {
$dom = new \DOMDocument();
$dom->loadXML($xslResource->getContents());
} catch (SystemResourceDoesNotExistException) {
return;
}
$hashes = [];
foreach ($dom->getElementsByTagName('style') as $node) {
if ($node->getAttribute('type') !== 'text/css') {
continue;
}
$hashes[] = HashValue::hash($node->textContent);
}
if ($hashes === []) {
return;
}
$this->policyRegistry->appendMutationCollection(
new MutationCollection(
new Mutation(
MutationMode::Extend,
Directive::StyleSrcElem,
...$hashes
)
)
);
}
}