From 8bcdec5d94e351189225e8b0afbe92f1d68e40b2 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Wed, 7 Feb 2024 18:04:55 +0100 Subject: [PATCH] Finally migrate RSS Feed Misusing the sitemap implementation was a bad idea and broke for every major update. I now finally migrate to custom implementation which should remove the issues for future. --- .../RssFeed/BlogPostsDataProvider.php | 148 ++++++++++++++++ Classes/Frontend/RssFeed/ContentRendering.php | 64 ------- .../Frontend/RssFeed/SitemapDataProvider.php | 105 ----------- .../Frontend/RssFeed/XmlSitemapRenderer.php | 167 ------------------ Configuration/Services.yaml | 2 +- .../TypoScript/Setup/RssFeed.typoscript | 77 ++++---- .../Templates/{Sitemaps => }/RssFeed.xml | 20 +-- 7 files changed, 197 insertions(+), 386 deletions(-) create mode 100644 Classes/Frontend/RssFeed/BlogPostsDataProvider.php delete mode 100644 Classes/Frontend/RssFeed/ContentRendering.php delete mode 100644 Classes/Frontend/RssFeed/SitemapDataProvider.php delete mode 100644 Classes/Frontend/RssFeed/XmlSitemapRenderer.php rename Resources/Private/Templates/{Sitemaps => }/RssFeed.xml (65%) diff --git a/Classes/Frontend/RssFeed/BlogPostsDataProvider.php b/Classes/Frontend/RssFeed/BlogPostsDataProvider.php new file mode 100644 index 0000000..84249d7 --- /dev/null +++ b/Classes/Frontend/RssFeed/BlogPostsDataProvider.php @@ -0,0 +1,148 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace DanielSiepmann\DsSite\Frontend\RssFeed; + +use Exception; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; +use TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor; + +final class BlogPostsDataProvider implements DataProcessorInterface +{ + public function __construct( + private readonly DatabaseQueryProcessor $databaseQueryProcessor, + private readonly ConnectionPool $connectionPool + ) { + } + + public function process( + ContentObjectRenderer $contentObjectRenderer, + array $contentObjectConfiguration, + array $processorConfiguration, + array $processedData + ) { + $processedData = $this->databaseQueryProcessor->process( + $contentObjectRenderer, + $contentObjectConfiguration, + array_merge($processorConfiguration, [ + 'where' => 'AND no_index = 0' . $this->getAdditionalWhere($contentObjectRenderer->getRequest()), + ]), + $processedData + ); + + foreach ($processedData['pages'] as &$page) { + $page['description'] = $page['data']['abstract'] + . $this->getContent( + $contentObjectRenderer, + $page['data']['uid'] + ); + } + + return $processedData; + } + + private function getAdditionalWhere(ServerRequestInterface $request): string + { + $categoryUid = intval($request->getQueryParams()['category_uid'] ?? 0); + if ($categoryUid === 0) { + return ''; + } + + $pageUids = $this->getPageUidsWithRelationToCategory($categoryUid); + $where = $this->createAdditionalWhereForPageUids($pageUids); + + if ($where !== '') { + return ''; + } + return ' ' . $where; + } + + private function getPageUidsWithRelationToCategory(int $categoryUid): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + + $queryBuilder->select('uid'); + $queryBuilder->from('pages'); + $queryBuilder->leftJoin( + 'pages', + 'sys_category_record_mm', + 'mm', + 'pages.uid = mm.uid_foreign' + ); + $queryBuilder->where( + $queryBuilder->expr()->eq( + 'mm.tablenames', + $queryBuilder->createNamedParameter('pages') + ), + $queryBuilder->expr()->eq( + 'mm.fieldname', + $queryBuilder->createNamedParameter('categories') + ), + $queryBuilder->expr()->in( + 'mm.uid_local', + $queryBuilder->createNamedParameter($categoryUid) + ) + ); + + return array_map(function (array $row) { + if (is_numeric($row['uid'])) { + return (int) $row['uid']; + } + throw new Exception('UID was not numeric: ' . var_export($row['uid'], true), 1707325559); + }, $queryBuilder->executeQuery()->fetchAllAssociative()); + } + + private function createAdditionalWhereForPageUids(array $pageUids): string + { + return ' AND uid IN(' . implode(',', $pageUids) . ')'; + } + + private function getContent( + ContentObjectRenderer $contentObjectRenderer, + int $pageUid + ): string { + $colPositions = [ + 50, + 0, + 100, + 200, + ]; + + $content = ''; + foreach ($colPositions as $colPos) { + $content .= $contentObjectRenderer->cObjGetSingle('CONTENT', [ + 'table' => 'tt_content', + 'select.' => [ + 'orderBy' => 'sorting', + 'where' => '{#colPos}=' . $colPos, + 'pidInList' => $pageUid, + ], + ]); + } + + return $content; + } +} diff --git a/Classes/Frontend/RssFeed/ContentRendering.php b/Classes/Frontend/RssFeed/ContentRendering.php deleted file mode 100644 index c07740e..0000000 --- a/Classes/Frontend/RssFeed/ContentRendering.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301, USA. - */ - -namespace DanielSiepmann\DsSite\Frontend\RssFeed; - -use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; - -class ContentRendering -{ - public function __construct( - private readonly ContentObjectRenderer $contentObjectRenderer - ) { - } - - public function extend(array $row): array - { - $row['description'] = $row['data']['abstract'] . $this->getContent($row['data']['uid']); - return $row; - } - - private function getContent(int $pageUid): string - { - $colPositions = [ - 50, - 0, - 100, - 200, - ]; - - $content = ''; - foreach ($colPositions as $colPos) { - $content .= $this->contentObjectRenderer->cObjGetSingle('CONTENT', [ - 'table' => 'tt_content', - 'select.' => [ - 'orderBy' => 'sorting', - 'where' => '{#colPos}=' . $colPos, - 'pidInList' => $pageUid, - ], - ]); - } - - return $content; - } -} diff --git a/Classes/Frontend/RssFeed/SitemapDataProvider.php b/Classes/Frontend/RssFeed/SitemapDataProvider.php deleted file mode 100644 index aaf21d5..0000000 --- a/Classes/Frontend/RssFeed/SitemapDataProvider.php +++ /dev/null @@ -1,105 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301, USA. - */ - -namespace DanielSiepmann\DsSite\Frontend\RssFeed; - -use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Core\Database\Connection; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\QueryBuilder; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; -use TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider; - -/** - * Provides dynamic for additionalWhere, - * to only list pages for current requested category. - */ -class SitemapDataProvider extends RecordsXmlSitemapDataProvider -{ - public function __construct( - ServerRequestInterface $request, - string $key, - array $config = [], - ContentObjectRenderer $cObj = null - ) { - $categoryUid = intval($request->getQueryParams()['category_uid'] ?? 0); - if ($categoryUid > 0) { - $pageUids = $this->getPageUidsWithRelationToCategory($categoryUid); - $config['additionalWhere'] = ($config['additionalWhere'] ?? '') - . $this->createAdditionalWhereForPageUids($pageUids) - ; - } - - parent::__construct($request, $key, $config, $cObj); - } - - public function generateItems(): void - { - parent::generateItems(); - - $contentRendering = new ContentRendering($this->cObj); - foreach ($this->items as $key => $item) { - $this->items[$key] = $contentRendering->extend($item); - } - } - - private function getPageUidsWithRelationToCategory(int $categoryUid): array - { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('pages'); - /* @var QueryBuilder $queryBuilder */ - - $queryBuilder->select('uid'); - $queryBuilder->from('pages'); - $queryBuilder->leftJoin( - 'pages', - 'sys_category_record_mm', - 'mm', - 'pages.uid = mm.uid_foreign' - ); - $queryBuilder->where( - $queryBuilder->expr()->eq( - 'mm.tablenames', - $queryBuilder->createNamedParameter('pages') - ), - $queryBuilder->expr()->eq( - 'mm.fieldname', - $queryBuilder->createNamedParameter('categories') - ), - $queryBuilder->expr()->in( - 'mm.uid_local', - $queryBuilder->createNamedParameter($categoryUid) - ) - ); - - return array_map(function (array $row) { - return (int) $row['uid']; - }, $queryBuilder->executeQuery()->fetchAllAssociative()); - } - - private function createAdditionalWhereForPageUids(array $pageUids): string - { - return ' AND uid IN(' . implode(',', $pageUids) . ')'; - } -} diff --git a/Classes/Frontend/RssFeed/XmlSitemapRenderer.php b/Classes/Frontend/RssFeed/XmlSitemapRenderer.php deleted file mode 100644 index 2796326..0000000 --- a/Classes/Frontend/RssFeed/XmlSitemapRenderer.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301, USA. - */ - -namespace DanielSiepmann\DsSite\Frontend\RssFeed; - -use Psr\Http\Message\ServerRequestInterface; -use TYPO3Fluid\Fluid\View\TemplateView; -use TYPO3\CMS\Core\Http\PropagateResponseException; -use TYPO3\CMS\Core\TypoScript\TypoScriptService; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory; -use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; -use TYPO3\CMS\Frontend\Controller\ErrorController; -use TYPO3\CMS\Seo\XmlSitemap\Exception\InvalidConfigurationException; -use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface; - -class XmlSitemapRenderer -{ - private array $typoScriptConfiguration = []; - - protected array $configuration; - - private ContentObjectRenderer $contentObjectRenderer; - - protected TemplateView $view; - - public function __construct( - protected TypoScriptService $typoScriptService, - protected RenderingContextFactory $renderingContextFactory, - ) { - } - - public function setContentObjectRenderer(ContentObjectRenderer $contentObjectRenderer): void - { - $this->contentObjectRenderer = $contentObjectRenderer; - } - - protected function initialize(array $fullConfiguration): void - { - $this->configuration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($fullConfiguration['plugin.']['tx_seo.'] ?? []); - $renderingContext = $this->renderingContextFactory->create(); - $templatePaths = $renderingContext->getTemplatePaths(); - $templatePaths->setTemplateRootPaths($this->configuration['view']['templateRootPaths']); - $templatePaths->setLayoutRootPaths($this->configuration['view']['layoutRootPaths']); - $templatePaths->setPartialRootPaths($this->configuration['view']['partialRootPaths']); - $templatePaths->setFormat('xml'); - $this->view = GeneralUtility::makeInstance(TemplateView::class, $renderingContext); - $this->view->assign('settings', $this->getSettings()); - } - - public function render(string $_, array $typoScriptConfiguration, ServerRequestInterface $request): string - { - $this->typoScriptConfiguration = $typoScriptConfiguration; - - $this->initialize($GLOBALS['TSFE']->tmpl->setup); - $this->view->assign('type', $request->getAttribute('routing')?->getPageType() ?? 0); - $sitemapType = $typoScriptConfiguration['sitemapType'] ?? 'xmlSitemap'; - if (!empty($sitemap = ($request->getQueryParams()['sitemap'] ?? null))) { - return $this->renderSitemap($request, $sitemap, $sitemapType); - } - - return $this->renderIndex($request, $sitemapType); - } - - protected function renderIndex(ServerRequestInterface $request, string $sitemapType): string - { - $sitemaps = []; - foreach ($this->configuration['config'][$sitemapType]['sitemaps'] ?? [] as $sitemap => $config) { - if (!empty($config['provider']) && is_string($config['provider']) - && class_exists($config['provider']) - && is_subclass_of($config['provider'], XmlSitemapDataProviderInterface::class) - ) { - /** @var XmlSitemapDataProviderInterface $provider */ - $provider = GeneralUtility::makeInstance( - $config['provider'], - $request, - $sitemap, - (array)($config['config'] ?? []) - ); - - $pages = $provider->getNumberOfPages(); - - for ($page = 0; $page < $pages; $page++) { - $sitemaps[] = [ - 'key' => $sitemap, - 'page' => $page, - 'lastMod' => $provider->getLastModified(), - ]; - } - } - } - - $this->view->assign('sitemapType', $sitemapType); - $this->view->assign('sitemaps', $sitemaps); - - return $this->view->render('Index'); - } - - protected function renderSitemap(ServerRequestInterface $request, string $sitemap, string $sitemapType): string - { - if (!empty($sitemapConfig = $this->configuration['config'][$sitemapType]['sitemaps'][$sitemap] ?? null)) { - if (class_exists($sitemapConfig['provider']) && - is_subclass_of($sitemapConfig['provider'], XmlSitemapDataProviderInterface::class)) { - /** @var XmlSitemapDataProviderInterface $provider */ - $provider = GeneralUtility::makeInstance( - $sitemapConfig['provider'], - $request, - $sitemap, - (array)($sitemapConfig['config'] ?? []) - ); - - $items = $provider->getItems(); - - $this->view->assign('items', $items); - $this->view->assign('sitemapType', $sitemapType); - - $template = ($sitemapConfig['config']['template'] ?? false) ?: 'Sitemap'; - return $this->view->render($template); - } - throw new InvalidConfigurationException('No valid provider set for ' . $sitemap, 1535578522); - } - - throw new PropagateResponseException( - GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'No valid configuration found for sitemap ' . $sitemap - ), - 1535578569 - ); - } - - private function getSettings(): array - { - $settings = []; - foreach (array_keys($this->typoScriptConfiguration['userFunc.']['variables.'] ?? []) as $variableName) { - if (!is_string($variableName) || substr($variableName, -1) === '.') { - continue; - } - $settings[$variableName] = $this->contentObjectRenderer->cObjGetSingle( - $this->typoScriptConfiguration['userFunc.']['variables.'][$variableName] ?? '', - $this->typoScriptConfiguration['userFunc.']['variables.'][$variableName . '.'] ?? [] - ); - } - - return $settings; - } -} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index b962b1f..a83eb84 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -18,7 +18,7 @@ services: arguments: - 'pages' - DanielSiepmann\DsSite\Frontend\RssFeed\XmlSitemapRenderer: + DanielSiepmann\DsSite\Frontend\RssFeed\BlogPostsDataProvider: public: true DanielSiepmann\DsSite\Backend\PreviewRenderer\Video: diff --git a/Configuration/TypoScript/Setup/RssFeed.typoscript b/Configuration/TypoScript/Setup/RssFeed.typoscript index f4fd429..e5984c4 100644 --- a/Configuration/TypoScript/Setup/RssFeed.typoscript +++ b/Configuration/TypoScript/Setup/RssFeed.typoscript @@ -1,30 +1,3 @@ -plugin.tx_seo { - view { - templateRootPaths { - 20 = EXT:ds_site/Resources/Private/Templates/Sitemaps/ - } - } - config { - xmlSitemap { - sitemaps { - blog-posts { - provider = DanielSiepmann\DsSite\Frontend\RssFeed\SitemapDataProvider - config { - table = pages - sortField = lastUpdated - sortOrder = DESC - lastModifiedField = tstamp - additionalWhere = AND no_index = 0 - pid = {$pageUids.blogPosts} - recursive = 3 - template = RssFeed - } - } - } - } - } -} - page { headerData { 10 = TEXT @@ -32,7 +5,7 @@ page { wrap = typolink { parameter = t3://page?uid=1 - additionalParams = &type=1533906435&sitemap=blog-posts + additionalParams = &type=1707321482&feed=blog-posts returnLast = url } } @@ -58,7 +31,7 @@ page { additionalParams.stdWrap.cObject = COA additionalParams.stdWrap.cObject { 10 = TEXT - 10.value = &type=1533906435&sitemap=blog-posts + 10.value = &type=1707321482&feed=blog-posts 11 = TEXT 11.value = &category_uid= 12 = TEXT @@ -71,18 +44,44 @@ page { } } } +rssFeed = PAGE +rssFeed { + typeNum = 1707321482 -seo_sitemap { - 10 { - userFunc = DanielSiepmann\DsSite\Frontend\RssFeed\XmlSitemapRenderer->render - userFunc { - variables { - categoryId = TEXT - categoryId.data = GP:category_uid - categoryId.intval = 1 - categoryTitle = TEXT - categoryTitle.data.dataWrap = DB : sys_category:{GP:category_uid}:title + config { + admPanel = 0 + debug = 0 + disableAllHeaderCode = 1 + additionalHeaders { + 10 { + header = Content-Type: application/xml;charset=utf-8 } } } + + 10 = FLUIDTEMPLATE + 10 { + dataProcessing { + 10 = DanielSiepmann\DsSite\Frontend\RssFeed\BlogPostsDataProvider + 10 { + as = pages + table = pages + orderBy = lastUpdated DESC + selectFields = pages.uid, pages.abstract, pages.title, pages.lastUpdated + where = AND no_index = 0 + pidInList = {$pageUids.blogPosts} + recursive = 3 + } + } + + variables { + categoryId = TEXT + categoryId.data = GP:category_uid + categoryId.intval = 1 + categoryTitle = TEXT + categoryTitle.data.dataWrap = DB : sys_category:{GP:category_uid}:title + } + + file = EXT:ds_site/Resources/Private/Templates/RssFeed.xml + } } diff --git a/Resources/Private/Templates/Sitemaps/RssFeed.xml b/Resources/Private/Templates/RssFeed.xml similarity index 65% rename from Resources/Private/Templates/Sitemaps/RssFeed.xml rename to Resources/Private/Templates/RssFeed.xml index b151360..efa3cc4 100644 --- a/Resources/Private/Templates/Sitemaps/RssFeed.xml +++ b/Resources/Private/Templates/RssFeed.xml @@ -1,28 +1,28 @@ - + - Daniel Siepmann - Coding is Art - Blog Posts {settings.categoryTitle} - List of {settings.categoryTitle} blog posts at daniel-siepmann.de - {f:uri.typolink(parameter: 't3://page?uid=11', additionalParams: '&topic_uid={settings.categoryId}', absolute: 1)} - + Daniel Siepmann - Coding is Art - Blog Posts {categoryTitle} + List of {categoryTitle} blog posts at daniel-siepmann.de + {f:uri.typolink(parameter: 't3://page?uid=11', additionalParams: '&topic_uid={categoryId}', absolute: 1)} + Daniel Siepmann - Coding is Art - All Blog Posts List of blog posts at daniel-siepmann.de {f:uri.typolink(parameter: 't3://page?uid=1', absolute: 1)} - + {f:format.date(date: 'now', format: 'D, d M Y H:i:s O')} 1800 - - + + {f:render(section: 'Item', arguments: { - item: item.data, - description: item.description + item: post.data, + description: post.description })}