TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user