From 34f2d55f2c67662d5870516a90f9ef8e39c41659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 21 Oct 2026 14:34:10 +0200 Subject: [PATCH] fix: render product detail pages on TYPO3 13 - ObjectManager replaced by GeneralUtility::makeInstance(), the services fetched that way are public - QueryBuilder::add('orderBy') and the remaining execute() calls, detailAction returns a response - PHP 8 warnings for missing array keys (same values as on PHP 7.4) - Webcam images are external URLs: plain img tag, f:image only takes FAL files - typo3_encore 7 needs the PKG:ssch/typo3-encore: prefix and external = 1 so TYPO3's path check does not drop the entry - CDN image rewriting as a middleware instead of EXT:replacer, which is not PHP 8.5 clean; same pattern, only for page responses - vhs 8 for the ViewHelpers the templates use - OpenGraph meta tag manager skips the backend - myEpUrl with trailing slash as in production Co-Authored-By: Claude Opus 5.5 --- composer.json | 1 + composer.lock | 72 ++++++++++++++++++- config/system/settings.php | 5 +- .../Classes/ContentFeed/RecordFinder.php | 2 +- .../Classes/Controller/ProductController.php | 5 +- .../Domain/Repository/AbstractRepository.php | 4 +- .../Domain/Repository/HotelRepository.php | 14 ++-- .../Domain/Repository/ProductRepository.php | 8 +-- .../Classes/Service/AbstractImageService.php | 2 +- .../Classes/Service/HotelDataService.php | 4 +- .../RequestArgumentTypeConversionTrait.php | 9 +-- .../Classes/Utility/SerializationUtility.php | 5 +- .../Classes/Utility/SettingsUtility.php | 5 +- .../ep_products/Configuration/Services.yaml | 20 ++++++ .../DataProcessing/ContextProcessor.php | 5 +- .../DataProcessing/FlexFormProcessor.php | 2 +- .../DataProcessing/TeaserProcessor.php | 15 ++-- .../MetaTag/OpenGraphMetaTagManager.php | 7 +- .../Classes/Middleware/CdnImageMiddleware.php | 65 +++++++++++++++++ .../Configuration/RequestMiddlewares.php | 12 ++++ .../TypoScript/Page/Main.typoscript | 8 ++- .../Private/Partials/Region/Webcam.html | 7 +- packages/ep_theme/ext_conf_template.txt | 2 + 23 files changed, 222 insertions(+), 57 deletions(-) create mode 100644 packages/ep_theme/Classes/Middleware/CdnImageMiddleware.php create mode 100644 packages/ep_theme/Configuration/RequestMiddlewares.php create mode 100644 packages/ep_theme/ext_conf_template.txt diff --git a/composer.json b/composer.json index 0e6d7ca..8afa684 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "b13/container": "^4.1", "ep/ep-products": "@dev", "ep/ep-theme": "@dev", + "fluidtypo3/vhs": "^8.0", "ssch/typo3-encore": "^7.0", "typo3/cms-backend": "^13.4", "typo3/cms-belog": "^13.4", diff --git a/composer.lock b/composer.lock index f30bbc5..5905dbc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bb6164470dbc546b9cb0a9b2d31797f2", + "content-hash": "9de2907eea881944a362013a40f3cc7e", "packages": [ { "name": "b13/container", @@ -976,6 +976,76 @@ }, "time": "2026-09-28T17:51:46+00:00" }, + { + "name": "fluidtypo3/vhs", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/FluidTYPO3/vhs.git", + "reference": "cdf53ec693d8a69a8b120d89b01aff2281b0b049" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FluidTYPO3/vhs/zipball/cdf53ec693d8a69a8b120d89b01aff2281b0b049", + "reference": "cdf53ec693d8a69a8b120d89b01aff2281b0b049", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.3", + "typo3/cms-backend": "^12 || ^13 || ^14.3 || dev-main", + "typo3/cms-core": "^12 || ^13 || ^14.3 || dev-main", + "typo3/cms-extbase": "^12 || ^13 || ^14.3 || dev-main", + "typo3/cms-fluid": "^12 || ^13 || ^14.3 || dev-main", + "typo3/cms-frontend": "^12 || ^13 || ^14.3 || dev-main" + }, + "replace": { + "typo3-ter/vhs": "self.version" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpstan/phpstan": "1.12.1", + "phpunit/phpunit": "^5.7 || ^9.5", + "squizlabs/php_codesniffer": "^3.7" + }, + "suggest": { + "ext-json": "Enable use of v:format.json.encode and v:format.json.decode", + "ext-tidy": "Allows to make use of the tidy ViewHelper v:format.tidy", + "ext-zlib": "Enable use of v:format.placeholder.lipsum with default lipsum text" + }, + "type": "typo3-cms-extension", + "extra": { + "typo3/cms": { + "extension-key": "vhs" + } + }, + "autoload": { + "psr-4": { + "FluidTYPO3\\Vhs\\": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "This is a collection of ViewHelpers for performing rendering tasks that are not natively provided by TYPO3's Fluid templating engine.", + "homepage": "https://fluidtypo3.org", + "keywords": [ + "TYPO3 CMS", + "fedext", + "fluid", + "templating", + "utility", + "viewhelper" + ], + "support": { + "chat": "https://typo3.slack.com/archives/C79562JES", + "docs": "https://docs.typo3.org/p/fluidtypo3/vhs/main/en-us/", + "issues": "https://github.com/FluidTYPO3/vhs/issues", + "source": "https://github.com/FluidTYPO3/vhs" + }, + "time": "2026-07-23T10:57:34+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "8.3.0", diff --git a/config/system/settings.php b/config/system/settings.php index 2c1279a..dbb4ed3 100644 --- a/config/system/settings.php +++ b/config/system/settings.php @@ -54,9 +54,12 @@ return [ 'myEpApiUrlAuthorize' => '', 'myEpApiUrlToken' => '', 'myEpStagingUrl' => 'https://my.ep-reisen.net', - 'myEpUrl' => 'https://my.ep-reisen.de', + 'myEpUrl' => 'https://my.ep-reisen.de/', 'searchPageUid' => '0', ], + 'ep_theme' => [ + 'cdnBaseUrl' => 'https://cdn.ep-reisen.de/', + ], 'extensionmanager' => [ 'automaticInstallation' => '1', 'offlineMode' => '0', diff --git a/packages/ep_products/Classes/ContentFeed/RecordFinder.php b/packages/ep_products/Classes/ContentFeed/RecordFinder.php index 9de30d4..f5668e9 100644 --- a/packages/ep_products/Classes/ContentFeed/RecordFinder.php +++ b/packages/ep_products/Classes/ContentFeed/RecordFinder.php @@ -65,7 +65,7 @@ class RecordFinder $rows = []; - foreach ($queryBuilder->execute()->fetchAllAssociative() as $row) { + foreach ($queryBuilder->executeQuery()->fetchAllAssociative() as $row) { $rows[(int) $row['uid']] = $row; } diff --git a/packages/ep_products/Classes/Controller/ProductController.php b/packages/ep_products/Classes/Controller/ProductController.php index a14d8a7..9bc3f8a 100644 --- a/packages/ep_products/Classes/Controller/ProductController.php +++ b/packages/ep_products/Classes/Controller/ProductController.php @@ -127,7 +127,7 @@ class ProductController extends ActionController ?Hotel $hotel = null, $dateFrom = null, $dateTo = null - ) { + ): ResponseInterface { // Get product to display from flexform settings if not provided via url if ($product === null) { $productUid = $this->settings['productUid']; @@ -175,7 +175,7 @@ class ProductController extends ActionController // Assemble backlink $backlinkUrl = $this->request->getQueryParams()['ref'] ?? null; - $backLinkLabel = strpos($backlinkUrl, 'suche') !== -1 ? 'zurück zum Suchergebnis' : 'zurück'; + $backLinkLabel = strpos((string)$backlinkUrl, 'suche') !== -1 ? 'zurück zum Suchergebnis' : 'zurück'; $this->view->assignMultiple([ 'hotel' => $hotel, @@ -193,6 +193,7 @@ class ProductController extends ActionController 'nonBookableInfo' => $nonBookableInfo, 'pickupsOverview' => $this->pickupOverviewService->getOverview($product, $hotel), ]); + return $this->htmlResponse(); } public function pricetableAction(): ResponseInterface diff --git a/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php b/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php index 51f79e6..d3ed3a0 100644 --- a/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php +++ b/packages/ep_products/Classes/Domain/Repository/AbstractRepository.php @@ -9,6 +9,7 @@ use EP\EpProducts\Service\FilterService; use EP\EpProducts\Traits\DbConnectionTrait; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings; use TYPO3\CMS\Extbase\Persistence\Repository; @@ -18,8 +19,7 @@ abstract class AbstractRepository extends Repository public function initializeObject(): void { - /** @var $querySettings \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings */ - $querySettings = $this->objectManager->get(Typo3QuerySettings::class); + $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class); $querySettings->setRespectStoragePage(false); $this->setDefaultQuerySettings($querySettings); } diff --git a/packages/ep_products/Classes/Domain/Repository/HotelRepository.php b/packages/ep_products/Classes/Domain/Repository/HotelRepository.php index c739eb8..a7e9425 100644 --- a/packages/ep_products/Classes/Domain/Repository/HotelRepository.php +++ b/packages/ep_products/Classes/Domain/Repository/HotelRepository.php @@ -94,7 +94,7 @@ class HotelRepository extends AbstractRepository ->setParameter('productUid', $productUid) ; - return $qb->execute()->fetchAllAssociative(); + return $qb->executeQuery()->fetchAllAssociative(); } /** @@ -146,7 +146,7 @@ class HotelRepository extends AbstractRepository ; } - return $qb->execute()->fetchAllAssociative(); + return $qb->executeQuery()->fetchAllAssociative(); } /** @@ -174,9 +174,10 @@ class HotelRepository extends AbstractRepository ->leftJoin('hotel', 'tx_epproducts_domain_model_country', 'country', 'hotel.country = country.uid') ->leftJoin('hotel', 'tx_epproducts_domain_model_region', 'region', 'hotel.region = region.uid') ->where('hotel.show_as_teaser = 1') - ->add('orderBy', 'FIELD(hotel.type, ' . Hotel::TYPE_SPORTS_CLUB . ') DESC') - ->addOrderBy('hotel.name', 'ASC') ; + // Raw expression: TYPO3's orderBy() would quote it as an identifier + $qb->getConcreteQueryBuilder()->orderBy('FIELD(hotel.type, ' . Hotel::TYPE_SPORTS_CLUB . ') DESC'); + $qb->addOrderBy('hotel.name', 'ASC'); if ($excludedHotelUids) { $qb @@ -216,7 +217,7 @@ class HotelRepository extends AbstractRepository ; } - return $qb->execute()->fetchAllAssociative(); + return $qb->executeQuery()->fetchAllAssociative(); } /** @@ -244,8 +245,9 @@ class HotelRepository extends AbstractRepository ->addGroupBy('date.hotel') ->addGroupBy('date.date_start') ->addGroupBy('date.nights') - ->add('orderBy', 'FIELD(date.hotel_type, ' . Hotel::TYPE_SPORTS_CLUB . ') DESC') ; + // Raw expression: TYPO3's orderBy() would quote it as an identifier + $qb->getConcreteQueryBuilder()->orderBy('FIELD(date.hotel_type, ' . Hotel::TYPE_SPORTS_CLUB . ') DESC'); if ($excludeNonSearchable) { $qb->where('date.product_searchable = 1'); diff --git a/packages/ep_products/Classes/Domain/Repository/ProductRepository.php b/packages/ep_products/Classes/Domain/Repository/ProductRepository.php index 39b7eae..714448c 100644 --- a/packages/ep_products/Classes/Domain/Repository/ProductRepository.php +++ b/packages/ep_products/Classes/Domain/Repository/ProductRepository.php @@ -104,7 +104,7 @@ class ProductRepository extends AbstractRepository ); } - if ($filterSettings['productsPid']) { + if (!empty($filterSettings['productsPid'])) { $productsPid = $filterSettings['productsPid']; $qb->andWhere($qb->expr()->in( 'date.product_pid', @@ -157,7 +157,7 @@ class ProductRepository extends AbstractRepository ->addOrderBy('date.date_start') ; - if ($filterSettings['productsPid']) { + if (!empty($filterSettings['productsPid'])) { $productsPid = (array) $filterSettings['productsPid']; $qb->andWhere($qb->expr()->in( 'date.product_pid', @@ -236,7 +236,7 @@ class ProductRepository extends AbstractRepository ) ; - if ($filterSettings['productsPid']) { + if (!empty($filterSettings['productsPid'])) { $productsPid = (array) $filterSettings['productsPid']; $qb->andWhere($qb->expr()->in( 'date.product_pid', @@ -294,7 +294,7 @@ class ProductRepository extends AbstractRepository ->groupBy('date.date_start') ; - if ($filterSettings['productsPid']) { + if (!empty($filterSettings['productsPid'])) { $productsPid = (array) $filterSettings['productsPid']; $qb->andWhere($qb->expr()->in( 'date.product_pid', diff --git a/packages/ep_products/Classes/Service/AbstractImageService.php b/packages/ep_products/Classes/Service/AbstractImageService.php index 64a0aa3..af97bc6 100644 --- a/packages/ep_products/Classes/Service/AbstractImageService.php +++ b/packages/ep_products/Classes/Service/AbstractImageService.php @@ -108,7 +108,7 @@ abstract class AbstractImageService implements SingletonInterface { $cropString = $image->getProperty('crop'); $cropVariantCollection = CropVariantCollection::create((string) $cropString); - $cropVariant = $processingInstructions['cropVariant'] ?: 'default'; + $cropVariant = ($processingInstructions['cropVariant'] ?? null) ?: 'default'; $cropArea = $cropVariantCollection->getCropArea($cropVariant); return $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($image); diff --git a/packages/ep_products/Classes/Service/HotelDataService.php b/packages/ep_products/Classes/Service/HotelDataService.php index 47a0d4f..e5fa649 100644 --- a/packages/ep_products/Classes/Service/HotelDataService.php +++ b/packages/ep_products/Classes/Service/HotelDataService.php @@ -82,8 +82,8 @@ class HotelDataService implements SingletonInterface $hotelCategory = array_key_exists($hotelData['hotelCategory'], Hotel::$categoryLabels) ? Hotel::$categoryLabels[$hotelData['hotelCategory']] : null; - $dateStart = new \DateTime($hotelData['dateStart']); - $dateEnd = new \DateTime($hotelData['dateEnd']); + $dateStart = new \DateTime($hotelData['dateStart'] ?? 'now'); + $dateEnd = new \DateTime($hotelData['dateEnd'] ?? 'now'); return [ 'minDateStart' => $dateStart, diff --git a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php index 69c44c4..9104d9b 100644 --- a/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php +++ b/packages/ep_products/Classes/Traits/RequestArgumentTypeConversionTrait.php @@ -5,18 +5,15 @@ namespace EP\EpProducts\Traits; use EP\EpProducts\Service\FilterService; use EP\EpProducts\Service\SearchResultService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; trait RequestArgumentTypeConversionTrait { public function processSearchParamsArgument(): void { - /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); /** @var SearchResultService $searchResultService */ - $searchResultService = $objectManager->get(SearchResultService::class); + $searchResultService = GeneralUtility::makeInstance(SearchResultService::class); if (!$this->request->hasArgument('searchParams')) { $searchParams = $searchResultService->getNormalizedSearchParams(); @@ -32,11 +29,9 @@ trait RequestArgumentTypeConversionTrait public function processFilterSettingsArgument(array $forcedConceptUids = []): void { - /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); /** @var FilterService $filterService */ - $filterService = $objectManager->get(FilterService::class); + $filterService = GeneralUtility::makeInstance(FilterService::class); if (!$this->request->hasArgument('filterSettings')) { $filterSettings = $filterService->getNormalizedFilterSettings([], $forcedConceptUids); diff --git a/packages/ep_products/Classes/Utility/SerializationUtility.php b/packages/ep_products/Classes/Utility/SerializationUtility.php index 1b2ae7d..7db13bf 100644 --- a/packages/ep_products/Classes/Utility/SerializationUtility.php +++ b/packages/ep_products/Classes/Utility/SerializationUtility.php @@ -29,7 +29,6 @@ namespace EP\EpProducts\Utility; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; use TYPO3\CMS\Extbase\Service\ImageService; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -97,9 +96,7 @@ class SerializationUtility protected static function getImageService() { if (static::$imageService === null) { - /** @var ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); - static::$imageService = $objectManager->get(ImageService::class); + static::$imageService = GeneralUtility::makeInstance(ImageService::class); } return static::$imageService; diff --git a/packages/ep_products/Classes/Utility/SettingsUtility.php b/packages/ep_products/Classes/Utility/SettingsUtility.php index 4e6fecd..8a2bfb7 100644 --- a/packages/ep_products/Classes/Utility/SettingsUtility.php +++ b/packages/ep_products/Classes/Utility/SettingsUtility.php @@ -30,7 +30,6 @@ namespace EP\EpProducts\Utility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; -use TYPO3\CMS\Extbase\Object\ObjectManager; class SettingsUtility { @@ -41,9 +40,7 @@ class SettingsUtility public function __construct() { - /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); - $configurationManager = $objectManager->get(ConfigurationManager::class); + $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); $settings = GeneralUtility::removeDotsFromTS( $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT) ); diff --git a/packages/ep_products/Configuration/Services.yaml b/packages/ep_products/Configuration/Services.yaml index 75fc664..85944fa 100644 --- a/packages/ep_products/Configuration/Services.yaml +++ b/packages/ep_products/Configuration/Services.yaml @@ -11,6 +11,26 @@ services: EP\EpProducts\Service\ProductImportSource: alias: EP\EpProducts\Service\ApiImportSource + # Fetched with GeneralUtility::makeInstance() by data processors, view helpers and utilities + EP\EpProducts\Domain\Repository\CityRepository: + public: true + EP\EpProducts\Domain\Repository\ConceptRepository: + public: true + EP\EpProducts\Domain\Repository\CountryRepository: + public: true + EP\EpProducts\Domain\Repository\RegionRepository: + public: true + EP\EpProducts\Service\FilterService: + public: true + EP\EpProducts\Service\HotelDataService: + public: true + EP\EpProducts\Service\ProductDataService: + public: true + EP\EpProducts\Service\ResellerDataService: + public: true + EP\EpProducts\Service\SearchResultService: + public: true + cache.ep_cache: class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface public: true diff --git a/packages/ep_theme/Classes/DataProcessing/ContextProcessor.php b/packages/ep_theme/Classes/DataProcessing/ContextProcessor.php index 176a6cb..68d1ffa 100644 --- a/packages/ep_theme/Classes/DataProcessing/ContextProcessor.php +++ b/packages/ep_theme/Classes/DataProcessing/ContextProcessor.php @@ -30,7 +30,6 @@ namespace EP\EpTheme\DataProcessing; use EP\EpProducts\Service\ResellerDataService; use EP\EpProducts\Traits\DbConnectionTrait; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; @@ -77,10 +76,8 @@ class ContextProcessor implements DataProcessorInterface */ private function getResellerData() { - /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); /** @var ResellerDataService $resellerService */ - $resellerService = $objectManager->get(ResellerDataService::class); + $resellerService = GeneralUtility::makeInstance(ResellerDataService::class); return $resellerService->getResellerForCurrentDomain(); } diff --git a/packages/ep_theme/Classes/DataProcessing/FlexFormProcessor.php b/packages/ep_theme/Classes/DataProcessing/FlexFormProcessor.php index 55e3790..140875e 100644 --- a/packages/ep_theme/Classes/DataProcessing/FlexFormProcessor.php +++ b/packages/ep_theme/Classes/DataProcessing/FlexFormProcessor.php @@ -52,7 +52,7 @@ class FlexFormProcessor implements DataProcessorInterface $data = $cObj->data; if (array_key_exists('pi_flexform', $data)) { $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $processedData['data']['flexForm'] = $flexFormService->convertFlexFormContentToArray($data['pi_flexform']); + $processedData['data']['flexForm'] = $flexFormService->convertFlexFormContentToArray((string)$data['pi_flexform']); } return $processedData; } diff --git a/packages/ep_theme/Classes/DataProcessing/TeaserProcessor.php b/packages/ep_theme/Classes/DataProcessing/TeaserProcessor.php index 0678176..c1cf214 100644 --- a/packages/ep_theme/Classes/DataProcessing/TeaserProcessor.php +++ b/packages/ep_theme/Classes/DataProcessing/TeaserProcessor.php @@ -37,7 +37,6 @@ use EP\EpProducts\Domain\Repository\RegionRepository; use EP\EpProducts\Service\HotelDataService; use EP\EpProducts\Service\ProductDataService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; @@ -58,40 +57,38 @@ class TeaserProcessor implements DataProcessorInterface array $processorConfiguration, array $processedData ) { - /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); if ((int) $processedData['data']['tx_eptheme_context_city'] > 0) { /** @var CityRepository $repository */ - $repository = $objectManager->get(CityRepository::class); + $repository = GeneralUtility::makeInstance(CityRepository::class); /** @var City $city */ $city = $repository->findByIdentifier($processedData['data']['tx_eptheme_context_city']); $processedData['city'] = $city; } if ((int) $processedData['data']['tx_eptheme_context_region'] > 0) { /** @var RegionRepository $repository */ - $repository = $objectManager->get(RegionRepository::class); + $repository = GeneralUtility::makeInstance(RegionRepository::class); /** @var Region $region */ $region = $repository->findByIdentifier($processedData['data']['tx_eptheme_context_region']); $processedData['region'] = $region; } if ((int) $processedData['data']['tx_eptheme_context_country'] > 0) { /** @var CountryRepository $repository */ - $repository = $objectManager->get(CountryRepository::class); + $repository = GeneralUtility::makeInstance(CountryRepository::class); /** @var Country $country */ $country = $repository->findByIdentifier($processedData['data']['tx_eptheme_context_country']); $processedData['country'] = $country; } if ((int) $processedData['data']['tx_eptheme_context_hotel'] > 0) { /** @var HotelDataService $service */ - $service = $objectManager->get(HotelDataService::class); + $service = GeneralUtility::makeInstance(HotelDataService::class); /** @var array $hotel */ $hotel = $service->getTeaser($processedData['data']['tx_eptheme_context_hotel']); $processedData['hotel'] = $hotel; } if ((int) $processedData['data']['tx_eptheme_context_concept'] > 0) { /** @var ConceptRepository $repository */ - $repository = $objectManager->get(ConceptRepository::class); + $repository = GeneralUtility::makeInstance(ConceptRepository::class); /** @var Concept $concept */ $concept = $repository->findByIdentifier($processedData['data']['tx_eptheme_context_concept']); $processedData['concept'] = $concept; @@ -119,7 +116,7 @@ class TeaserProcessor implements DataProcessorInterface } /** @var ProductDataService $productDataService */ - $productDataService = $objectManager->get(ProductDataService::class); + $productDataService = GeneralUtility::makeInstance(ProductDataService::class); $processedData['teaser'] = $productDataService->getTeaser($filterSettings); $processedData['teaser']['forceHotelUid'] = $hotelUid; } diff --git a/packages/ep_theme/Classes/MetaTag/OpenGraphMetaTagManager.php b/packages/ep_theme/Classes/MetaTag/OpenGraphMetaTagManager.php index 076ae60..5def35f 100644 --- a/packages/ep_theme/Classes/MetaTag/OpenGraphMetaTagManager.php +++ b/packages/ep_theme/Classes/MetaTag/OpenGraphMetaTagManager.php @@ -55,7 +55,12 @@ class OpenGraphMetaTagManager extends \TYPO3\CMS\Seo\MetaTag\OpenGraphMetaTagMan */ protected function getPageWithImage() { - $rootLineUtility = new RootlineUtility($GLOBALS['TYPO3_REQUEST']->getAttribute('frontend.page.information')->getId()); + // The manager is also asked to render in the backend, where there is no frontend page + $pageInformation = $GLOBALS['TYPO3_REQUEST']?->getAttribute('frontend.page.information'); + if ($pageInformation === null) { + return null; + } + $rootLineUtility = new RootlineUtility($pageInformation->getId()); $rootLine = $rootLineUtility->get(); foreach ($rootLine as $page) { diff --git a/packages/ep_theme/Classes/Middleware/CdnImageMiddleware.php b/packages/ep_theme/Classes/Middleware/CdnImageMiddleware.php new file mode 100644 index 0000000..c339ebc --- /dev/null +++ b/packages/ep_theme/Classes/Middleware/CdnImageMiddleware.php @@ -0,0 +1,65 @@ + 504 errorcode 106 until that key is purged, while the origin + * serves the same bytes in under 100ms). For an image that degrades one element; for + * js/main..js it would take every visitor's JavaScript down. + * + * typo3temp is scoped to generated images on purpose - assets/compressed holds other + * extensions' concatenated css/js, which belongs on the origin for the same reason. + * The quote character is captured rather than matched literally, so url() values + * written with single quotes are rewritten too. + * + * Registered after the page resolver, so the /api/* responses of ep_products never pass here. + */ +final class CdnImageMiddleware implements MiddlewareInterface +{ + private const PATTERN = '/(["\'])\/?(fileadmin|uploads|typo3temp\/assets\/(?:_processed_|images))/'; + + public function __construct(private readonly ExtensionConfiguration $extensionConfiguration) + { + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $response = $handler->handle($request); + + $cdnBaseUrl = rtrim((string)($this->extensionConfiguration->get('ep_theme', 'cdnBaseUrl') ?? ''), '/'); + $contentType = $response->getHeaderLine('Content-Type'); + if ($cdnBaseUrl === '' + || !(str_starts_with($contentType, 'text/html') || str_starts_with($contentType, 'application/json')) + ) { + return $response; + } + + $body = (string)$response->getBody(); + $replaced = preg_replace(self::PATTERN, '$1' . $cdnBaseUrl . '/$2', $body); + if ($replaced === null || $replaced === $body) { + return $response; + } + + $stream = new Stream('php://temp', 'rw'); + $stream->write($replaced); + return $response->withBody($stream); + } +} diff --git a/packages/ep_theme/Configuration/RequestMiddlewares.php b/packages/ep_theme/Configuration/RequestMiddlewares.php new file mode 100644 index 0000000..c36a623 --- /dev/null +++ b/packages/ep_theme/Configuration/RequestMiddlewares.php @@ -0,0 +1,12 @@ + [ + 'ep/cdn-images' => [ + 'target' => \EP\EpTheme\Middleware\CdnImageMiddleware::class, + 'after' => [ + 'typo3/cms-frontend/page-resolver', + ], + ], + ], +]; diff --git a/packages/ep_theme/Configuration/TypoScript/Page/Main.typoscript b/packages/ep_theme/Configuration/TypoScript/Page/Main.typoscript index 38fdfe4..a6f1a97 100644 --- a/packages/ep_theme/Configuration/TypoScript/Page/Main.typoscript +++ b/packages/ep_theme/Configuration/TypoScript/Page/Main.typoscript @@ -73,12 +73,16 @@ page { # RequestHandler turns this into `compress => false`, which PageRendererHooks forwards to # typo3_encore's TagRenderer (whose own default it would otherwise override). includeCSS { - main = typo3_encore:EpThemeConfig:main + main = PKG:ssch/typo3-encore:EpThemeConfig:main + # Skips TYPO3's file path check, which would drop the entry name before typo3_encore's hook sees it + main.external = 1 main.disableCompression = 1 } includeJSFooter { - main = typo3_encore:EpThemeConfig:main + main = PKG:ssch/typo3-encore:EpThemeConfig:main + # Skips TYPO3's file path check, which would drop the entry name before typo3_encore's hook sees it + main.external = 1 main.defer = 1 main.disableCompression = 1 } diff --git a/packages/ep_theme/Resources/Private/Partials/Region/Webcam.html b/packages/ep_theme/Resources/Private/Partials/Region/Webcam.html index 4ba18aa..4f1351b 100644 --- a/packages/ep_theme/Resources/Private/Partials/Region/Webcam.html +++ b/packages/ep_theme/Resources/Private/Partials/Region/Webcam.html @@ -12,11 +12,8 @@ data-action="lightbox#open" data-lightbox-caption="Webcam {region.name}" title="Webcam {region.name}"> - + External image: f:image only resolves FAL files since TYPO3 12 + Webcam {region.name} diff --git a/packages/ep_theme/ext_conf_template.txt b/packages/ep_theme/ext_conf_template.txt new file mode 100644 index 0000000..4c53644 --- /dev/null +++ b/packages/ep_theme/ext_conf_template.txt @@ -0,0 +1,2 @@ +# cat=CDN; type=string; label=CDN base URL for images in frontend pages (fileadmin, uploads, generated typo3temp images); empty serves them from the site itself +cdnBaseUrl = https://cdn.ep-reisen.de/