Initial commit - Typo3 11.5.41

This commit is contained in:
Matteo Gallo
2026-07-03 17:53:31 +02:00
commit 5ca4743197
6811 changed files with 568848 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
/**
* Remove the hreflang for news in strict mode with no translations
*/
class HrefLangEvent
{
/** @var ContentObjectRenderer */
public $cObj;
/** @var LanguageMenuProcessor */
protected $languageMenuProcessor;
public function __construct(ContentObjectRenderer $cObj, LanguageMenuProcessor $languageMenuProcessor)
{
$this->cObj = $cObj;
$this->languageMenuProcessor = $languageMenuProcessor;
}
public function __invoke(ModifyHrefLangTagsEvent $event): void
{
$newsAvailabilityChecker = GeneralUtility::makeInstance(NewsAvailability::class);
if ($newsAvailabilityChecker->getNewsIdFromRequest() > 0) {
$allHrefLangs = $event->getHrefLangs();
$languages = $this->languageMenuProcessor->process($this->cObj, [], [], []);
$errorTriggered = false;
foreach ($languages['languagemenu'] as $language) {
$hreflangKey = $language['hreflang'];
// skip all languages which are not used in hreflang
if (!isset($allHrefLangs[$hreflangKey]) || $hreflangKey === 'x-default') {
continue;
}
try {
$check = $newsAvailabilityChecker->check($language['languageId']);
if (!$check) {
unset($allHrefLangs[$hreflangKey]);
}
} catch (\UnexpectedValueException $e) {
$errorTriggered = true;
}
}
if (!$errorTriggered) {
if (count($allHrefLangs) <= 2) {
unset($allHrefLangs['x-default']);
}
$event->setHrefLangs($allHrefLangs);
}
}
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Check if a news record is available
*/
class NewsAvailability
{
/**
* @param int $languageId
* @param int $newsId
* @return bool
*/
public function check(int $languageId, int $newsId = 0): bool
{
// get it from current request
if ($newsId === 0) {
$newsId = $this->getNewsIdFromRequest();
}
if ($newsId === 0) {
throw new \UnexpectedValueException('No news id provided', 1586431984);
}
/** @var SiteInterface $site */
$site = $this->getRequest()->getAttribute('site');
$allAvailableLanguagesOfSite = $site->getAllLanguages();
$targetLanguage = $this->getLanguageFromAllLanguages($allAvailableLanguagesOfSite, $languageId);
if (!$targetLanguage) {
throw new \UnexpectedValueException('Target language could not be found', 1586431985);
}
return $this->mustBeIncluded($newsId, $targetLanguage);
}
protected function mustBeIncluded(int $newsId, SiteLanguage $language): bool
{
if ($language->getFallbackType() === 'strict') {
$newsRecord = $this->getNewsRecord($newsId, $language->getLanguageId());
if (!is_array($newsRecord) || empty($newsRecord)) {
return false;
}
}
return true;
}
/**
* @param SiteLanguage[] $allLanguages
* @param int $languageId
*/
protected function getLanguageFromAllLanguages(array $allLanguages, int $languageId): ?SiteLanguage
{
foreach ($allLanguages as $siteLanguage) {
if ($siteLanguage->getLanguageId() === $languageId) {
return $siteLanguage;
}
}
return null;
}
protected function getNewsRecord(int $newsId, int $language)
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_news_domain_model_news');
if ($language === 0) {
$where = [
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT))
),
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
];
} else {
$where = [
$queryBuilder->expr()->orX(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
),
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT))
),
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT))
)
)
];
}
$row = $queryBuilder
->select('uid', 'l10n_parent', 'sys_language_uid')
->from('tx_news_domain_model_news')
->where(...$where)
->execute()
->fetch();
return $row ?: null;
}
/**
* @return ServerRequestInterface
*/
protected function getRequest(): ServerRequestInterface
{
return $GLOBALS['TYPO3_REQUEST'];
}
public function getNewsIdFromRequest(): int
{
$newsId = 0;
/** @var PageArguments $pageArguments */
$pageArguments = $this->getRequest()->getAttribute('routing');
if (isset($pageArguments->getRouteArguments()['tx_news_pi1']['news'])) {
$newsId = (int)$pageArguments->getRouteArguments()['tx_news_pi1']['news'];
} elseif (isset($this->getRequest()->getQueryParams()['tx_news_pi1']['news'])) {
$newsId = (int)$this->getRequest()->getQueryParams()['tx_news_pi1']['news'];
}
return $newsId;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\News;
use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generate page title based on properties of the news model
*/
class NewsTitleProvider extends AbstractPageTitleProvider
{
private const DEFAULT_PROPERTIES = 'title';
private const DEFAULT_GLUE = '" "';
/**
* @param News $news
* @param array $configuration
*/
public function setTitleByNews(News $news, array $configuration = []): void
{
$title = '';
$fields = GeneralUtility::trimExplode(',', $configuration['properties'] ?? self::DEFAULT_PROPERTIES, true);
foreach ($fields as $field) {
$getter = 'get' . ucfirst($field);
$value = $news->$getter();
if ($value) {
$title = $value;
break;
}
}
if ($title) {
$this->title = $title;
}
}
public function setTitle(string $title): void
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider;
use TYPO3\CMS\Seo\XmlSitemap\Exception\MissingConfigurationException;
/**
* Generate sitemap for news records
*/
class NewsXmlSitemapDataProvider extends AbstractXmlSitemapDataProvider
{
/**
* @param ServerRequestInterface $request
* @param string $key
* @param array $config
* @param ContentObjectRenderer|null $cObj
* @throws MissingConfigurationException
*/
public function __construct(ServerRequestInterface $request, string $key, array $config = [], ContentObjectRenderer $cObj = null)
{
parent::__construct($request, $key, $config, $cObj);
$this->generateItems();
}
/**
* @throws MissingConfigurationException
*/
public function generateItems(): void
{
$table = 'tx_news_domain_model_news';
$pids = !empty($this->config['pid']) ? GeneralUtility::intExplode(',', $this->config['pid']) : [];
$lastModifiedField = $this->config['lastModifiedField'] ?? 'tstamp';
$sortField = $this->config['sortField'] ?? 'datetime';
$forGoogleNews = $this->config['googleNews'] ?? false;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($table);
$constraints = [];
if (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField'])) {
$constraints[] = $queryBuilder->expr()->in(
$GLOBALS['TCA'][$table]['ctrl']['languageField'],
[
-1, // All languages
$this->getLanguageId() // Current language
]
);
}
if (!empty($pids)) {
$recursiveLevel = isset($this->config['recursive']) ? (int)$this->config['recursive'] : 0;
if ($recursiveLevel) {
$newList = [];
foreach ($pids as $pid) {
$list = $this->cObj->getTreeList($pid, $recursiveLevel);
if ($list) {
$newList = array_merge($newList, explode(',', $list));
}
}
$pids = array_merge($pids, $newList);
}
$constraints[] = $queryBuilder->expr()->in('pid', $pids);
}
if ($forGoogleNews) {
$constraints[] = $queryBuilder->expr()->gt($sortField, (new \DateTime('-2 days'))->getTimestamp());
}
if (!empty($this->config['excludedTypes'])) {
$excludedTypes = GeneralUtility::trimExplode(',', $this->config['excludedTypes'], true);
if (!empty($excludedTypes)) {
$constraints[] = $queryBuilder->expr()->notIn(
'type',
$queryBuilder->createNamedParameter($excludedTypes, Connection::PARAM_STR_ARRAY)
);
}
}
if (!empty($this->config['additionalWhere'])) {
$constraints[] = $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, $forGoogleNews ? 'DESC' : 'ASC')
->execute()
->fetchAll();
foreach ($rows as $row) {
$this->items[] = [
'data' => $row,
'lastMod' => (int)$row[$lastModifiedField],
'priority' => 0.5
];
}
}
/**
* @param array $data
* @return array
*/
protected function defineUrl(array $data): array
{
$pageId = $this->config['url']['pageId'] ?? $GLOBALS['TSFE']->id;
if ($this->config['url']['useCategorySinglePid'] && $pageIdFromCategory = $this->getSinglePidFromCategory($data['data']['uid'])) {
$pageId = $pageIdFromCategory;
}
$additionalParams = [];
$additionalParams = $this->getUrlFieldParameterMap($additionalParams, $data['data']);
$additionalParams = $this->getUrlAdditionalParams($additionalParams);
if (!empty($this->config['url']['hrDate']) && !empty($data['data']['datetime'])) {
// adjust timezone (database field is UTC)
$timezoneCorrectedDatetime = (int)$data['data']['datetime'] + date('Z', (int)$data['data']['datetime']);
$dateTime = \DateTime::createFromFormat('U', (string)$timezoneCorrectedDatetime);
if (!empty($this->config['url']['hrDate']['day'])) {
$additionalParams['tx_news_pi1[day]'] = $dateTime->format($this->config['url']['hrDate']['day']);
}
if (!empty($this->config['url']['hrDate']['month'])) {
$additionalParams['tx_news_pi1[month]'] = $dateTime->format($this->config['url']['hrDate']['month']);
}
if (!empty($this->config['url']['hrDate']['year'])) {
$additionalParams['tx_news_pi1[year]'] = $dateTime->format($this->config['url']['hrDate']['year']);
}
}
$additionalParamsString = http_build_query(
$additionalParams,
'',
'&',
PHP_QUERY_RFC3986
);
$typoLinkConfig = [
'parameter' => $pageId,
'additionalParams' => $additionalParamsString ? '&' . $additionalParamsString : '',
'forceAbsoluteUrl' => 1,
];
$data['loc'] = $this->cObj->typoLink_URL($typoLinkConfig);
return $data;
}
/**
* Obtains a pid for the single view from the category.
*
* @param int $newsId
* @return int
*/
protected function getSinglePidFromCategory(int $newsId): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$categoryRecord = $queryBuilder
->select('title', 'single_pid')
->from('sys_category')
->leftJoin(
'sys_category',
'sys_category_record_mm',
'sys_category_record_mm',
$queryBuilder->expr()->eq('sys_category_record_mm.uid_local', $queryBuilder->quoteIdentifier('sys_category.uid'))
)
->where(
$queryBuilder->expr()->eq('sys_category_record_mm.tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)),
$queryBuilder->expr()->gt('sys_category.single_pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_category_record_mm.uid_foreign', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
)
->setMaxResults(1)
->execute()->fetch();
return (int)$categoryRecord['single_pid'];
}
/**
* @param array $additionalParams
* @param array $data
* @return array
*/
protected function getUrlFieldParameterMap(array $additionalParams, array $data): array
{
if (!empty($this->config['url']['fieldToParameterMap']) &&
\is_array($this->config['url']['fieldToParameterMap'])) {
foreach ($this->config['url']['fieldToParameterMap'] as $field => $urlPart) {
$additionalParams[$urlPart] = $data[$field];
}
}
return $additionalParams;
}
/**
* @param array $additionalParams
* @return array
*/
protected function getUrlAdditionalParams(array $additionalParams): array
{
if (!empty($this->config['url']['additionalGetParameters']) &&
is_array($this->config['url']['additionalGetParameters'])) {
foreach ($this->config['url']['additionalGetParameters'] as $extension => $extensionConfig) {
foreach ($extensionConfig as $key => $value) {
$additionalParams[$extension . '[' . $key . ']'] = $value;
}
}
}
return $additionalParams;
}
/**
* @return int
* @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
*/
protected function getLanguageId(): int
{
$context = GeneralUtility::makeInstance(Context::class);
return (int)$context->getPropertyFromAspect('language', 'id');
}
/**
* @return WorkspaceAspect
*/
protected function getCurrentWorkspaceAspect(): WorkspaceAspect
{
return GeneralUtility::makeInstance(Context::class)->getAspect('workspace');
}
}