diff --git a/.gitattributes b/.gitattributes index 854f8bc..21900d7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ +/Build/ export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.travis.yml export-ignore -/phpunit.xml.dist export-ignore /Tests export-ignore +/.github export-ignore + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dbb2de8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + TYPO3: [ '14' ] + php: [ '8.2', '8.5'] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up PHP Version + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + + - name: Start MySQL + run: sudo /etc/init.d/mysql start + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Install composer dependencies + run: | + composer install --no-progress --no-interaction + - name: Phpstan + run: vendor/bin/phpstan analyze -c Build/phpstan.neon + - name: Phpcsfix + run: vendor/bin/php-cs-fixer fix --config=Build/php-cs-fixer.php --dry-run --stop-on-violation --using-cache=no + - name: Unit Tests + run: | + vendor/bin/phpunit -c Build/phpunit/UnitTests.xml Tests/Unit + - name: Functional Tests + run: | + export typo3DatabaseName="typo3"; + export typo3DatabaseHost="127.0.0.1"; + export typo3DatabaseUsername="root"; + export typo3DatabasePassword="root"; + vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml Tests/Functional diff --git a/.gitignore b/.gitignore index 342d335..2862eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -Build composer.lock +Build/phpunit/.phpunit.result.cache vendor public -.phpunit.result.cache +.php-cs-fixer.cache diff --git a/Build/php-cs-fixer.php b/Build/php-cs-fixer.php new file mode 100644 index 0000000..c699c6f --- /dev/null +++ b/Build/php-cs-fixer.php @@ -0,0 +1,11 @@ +getFinder()->exclude(['var', 'public', 'vendor'])->in(__DIR__ . '/..'); +$config->addRules([ + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, +]); +return $config; diff --git a/Build/phpstan.neon b/Build/phpstan.neon new file mode 100644 index 0000000..a7bd078 --- /dev/null +++ b/Build/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 5 + paths: + - %currentWorkingDirectory%/Classes + - %currentWorkingDirectory%/Tests diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..79f6e2e --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,24 @@ + + + + ../../Tests/Functional/ + + + + + + + diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..29a4cbb --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,21 @@ +defineOriginalRootPath(); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +}); diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml new file mode 100644 index 0000000..30215c6 --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,24 @@ + + + + ../../Tests/Unit/ + + + + + + + diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php new file mode 100644 index 0000000..5760477 --- /dev/null +++ b/Build/phpunit/UnitTestsBootstrap.php @@ -0,0 +1,68 @@ +getWebRoot(), '/')); + } + if (!getenv('TYPO3_PATH_WEB')) { + putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); + } + + $testbase->defineSitePath(); + + $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; + \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(0, $requestType); + + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient'); + + // Retrieve an instance of class loader and inject to core bootstrap + $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; + \TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader); + + // Initialize default TYPO3_CONF_VARS + $configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager(); + $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); + + $cache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend( + 'core', + new \TYPO3\CMS\Core\Cache\Backend\NullBackend([]) + ); + // Set all packages to active + $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, \TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache)); + + \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(\TYPO3\CMS\Core\Package\PackageManager::class, $packageManager); + \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager); + + $testbase->dumpClassLoadingInformation(); + + \TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances(); +})(); diff --git a/Build/sites/main/config.yaml b/Build/sites/main/config.yaml new file mode 100644 index 0000000..f1cfaaa --- /dev/null +++ b/Build/sites/main/config.yaml @@ -0,0 +1,20 @@ +base: 'http://localhost/' +baseVariants: { } +errorHandling: { } +languages: + - + title: english + enabled: true + base: / + typo3Language: default + locale: en_US.UTF-8 + iso-639-1: en + navigationTitle: '' + hreflang: '' + direction: '' + flag: global + languageId: '0' + websiteTitle: '' +rootPageId: 1 +routes: { } +websiteTitle: 'Testing' diff --git a/Classes/Http/ResourcePusher.php b/Classes/Http/ResourcePusher.php index dbc6bc3..37fa713 100644 --- a/Classes/Http/ResourcePusher.php +++ b/Classes/Http/ResourcePusher.php @@ -16,9 +16,11 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use TYPO3\CMS\Core\Cache\CacheDataCollector; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Utility\PathUtility; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Takes existing accumulated resources and pushes them as HTTP2 headers as middleware. @@ -30,15 +32,27 @@ */ class ResourcePusher implements MiddlewareInterface { + public function __construct( + #[Autowire(service: 'cache.tx_http2')] + private readonly FrontendInterface $cache + ) {} + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); - /** @var TypoScriptFrontendController $frontendController */ - $frontendController = $request->getAttribute('frontend.controller'); - $resources = $frontendController->config['b13/http2'] ?? null; + $response = $response->withHeader('foo', 'bar'); + + /** @var CacheDataCollector $cacheDataCollector */ + $cacheDataCollector = $request->getAttribute('frontend.cache.collector'); + $identifier = $cacheDataCollector->getPageCacheIdentifier(); + $resources = []; + if ($this->cache->has($identifier)) { + $resources = $this->cache->get($identifier); + } + /** @var NormalizedParams $normalizedParams */ $normalizedParams = $request->getAttribute('normalizedParams'); - if (is_array($resources) && $normalizedParams->isHttps()) { + if (!empty($resources) && $normalizedParams->isHttps()) { foreach ($resources['scripts'] ?? [] as $resource) { $response = $this->addPreloadHeaderToResponse($response, $resource, 'script'); } diff --git a/Classes/PageRendererHook.php b/Classes/PageRendererHook.php index b74fc61..8628655 100644 --- a/Classes/PageRendererHook.php +++ b/Classes/PageRendererHook.php @@ -13,10 +13,14 @@ */ use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use TYPO3\CMS\Core\Cache\CacheDataCollector; +use TYPO3\CMS\Core\Cache\CacheTag; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Hooks into PageRenderer before the rendering is taken care of, and remember the files @@ -24,9 +28,14 @@ * * This considers that everything is required, thus is marked as "preload", not via "prefetch". */ +#[Autoconfigure(public: true)] class PageRendererHook { - public function __construct(protected ResourceMatcher $matcher) {} + public function __construct( + #[Autowire(service: 'cache.tx_http2')] + private readonly FrontendInterface $cache, + protected ResourceMatcher $matcher + ) {} /** * @param array $params @@ -40,10 +49,8 @@ public function accumulateResources(array $params, PageRenderer $pageRenderer) return; } // If this is a second run (non-cached cObjects adding more data), then the existing cached data is fetched - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) { - /** @var TypoScriptFrontendController $frontendController */ - $frontendController = $request->getAttribute('frontend.controller'); - $allResources = $frontendController->config['b13/http2'] ?? []; + if (ApplicationType::fromRequest($request)->isFrontend()) { + $allResources = $this->getFromCached($request); } else { $allResources = []; } @@ -74,16 +81,35 @@ public function accumulateResources(array $params, PageRenderer $pageRenderer) */ protected function process(array $allResources, ServerRequestInterface $request): void { - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) { - /** @var TypoScriptFrontendController $frontendController */ - $frontendController = $request->getAttribute('frontend.controller'); - $frontendController->config['b13/http2'] = $allResources; + if (ApplicationType::fromRequest($request)->isFrontend()) { + $this->addToCached($request, $allResources); } elseif (GeneralUtility::getIndpEnv('TYPO3_SSL')) { // Push directly into the TYPO3 Backend, but only if TYPO3 is running in SSL GeneralUtility::makeInstance(ResourcePusher::class)->pushAll($allResources); } } + protected function getFromCached(ServerRequestInterface $request): array + { + /** @var CacheDataCollector $cacheDataCollector */ + $cacheDataCollector = $request->getAttribute('frontend.cache.collector'); + $identifier = $cacheDataCollector->getPageCacheIdentifier(); + if ($this->cache->has($identifier)) { + return $this->cache->get($identifier); + } + return []; + } + + protected function addToCached(ServerRequestInterface $request, array $data): void + { + /** @var CacheDataCollector $cacheDataCollector */ + $cacheDataCollector = $request->getAttribute('frontend.cache.collector'); + $identifier = $cacheDataCollector->getPageCacheIdentifier(); + $cacheTags = array_map(fn(CacheTag $cacheTag) => $cacheTag->name, $cacheDataCollector->getCacheTags()); + $cacheTimeout = $cacheDataCollector->resolveLifetime(); + $this->cache->set($identifier, $data, $cacheTags, $cacheTimeout); + } + protected function getRequest(): ?ServerRequestInterface { return $GLOBALS['TYPO3_REQUEST'] ?? null; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index b8227c0..9e45e73 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -7,5 +7,7 @@ services: B13\Http2\: resource: '../Classes/*' - B13\Http2\PageRendererHook: - public: true + cache.tx_http2: + class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface + factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache'] + arguments: ['tx_http2'] \ No newline at end of file diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg index 9496dfd..98fbc6b 100644 --- a/Resources/Public/Icons/Extension.svg +++ b/Resources/Public/Icons/Extension.svg @@ -1,35 +1 @@ - - - -ext_icon_crawler_transparent2 - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/Tests/Functional/Fixtures/Response.csv b/Tests/Functional/Fixtures/Response.csv new file mode 100644 index 0000000..b039862 --- /dev/null +++ b/Tests/Functional/Fixtures/Response.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title","slug" +,1,0,"root","/" +"sys_template" +,"uid","pid","root","config" +,1,1,1,"page = PAGE +page.10 = TEXT +page.includeJS.dummy-js = EXT:frontend/Resources/Public/JavaScript/default_frontend.js +" diff --git a/Tests/Functional/ResponseTest.php b/Tests/Functional/ResponseTest.php new file mode 100644 index 0000000..e878870 --- /dev/null +++ b/Tests/Functional/ResponseTest.php @@ -0,0 +1,40 @@ + 'typo3conf/sites']; + + #[Test] + public function linkHeaderExists(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/Response.csv'); + $request = new InternalRequest('http://localhost/'); + $request = $request->withServerParams(['HTTPS' => 'on']); + $response = $this->executeFrontendSubRequest($request); + self::assertTrue($response->hasHeader('link')); + $link = $response->getHeaderLine('link'); + self::assertStringContainsString('JavaScript/default_frontend.js', $link); + self::assertStringContainsString('>; rel=preload; as=script', $link); + // cached + $response = $this->executeFrontendSubRequest($request); + self::assertTrue($response->hasHeader('link')); + $link = $response->getHeaderLine('link'); + self::assertStringContainsString('JavaScript/default_frontend.js', $link); + self::assertStringContainsString('>; rel=preload; as=script', $link); + } +} diff --git a/composer.json b/composer.json index c22e615..066af7e 100644 --- a/composer.json +++ b/composer.json @@ -3,12 +3,15 @@ "type": "typo3-cms-extension", "description": "Speed up TYPO3 rendering via HTTP/2 Server Push", "require": { - "typo3/cms-core": "^12.4 || ^13.4", - "typo3/cms-frontend": "^12.4 || ^13.4" + "typo3/cms-core": "^14.1", + "typo3/cms-frontend": "^14.1" }, "require-dev": { "phpunit/phpunit": "^11.0", - "squizlabs/php_codesniffer": "^3.0" + "friendsofphp/php-cs-fixer": "^3.94", + "phpstan/phpstan": "^2.1", + "typo3/testing-framework": "^9.1", + "typo3/coding-standards": "^0.8.0" }, "homepage": "https://b13.com", "license": ["GPL-2.0-or-later"], @@ -32,11 +35,6 @@ "B13\\Http2\\Tests\\": "Tests" } }, - "scripts": { - "test": "phpunit", - "check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 Classes Tests", - "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 Classes Tests" - }, "config": { "allow-plugins": { "typo3/class-alias-loader": true, diff --git a/ext_localconf.php b/ext_localconf.php index 083208c..8e1879c 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,4 +1,9 @@ accumulateResources'; + +if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['tx_http2'] ?? null)) { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['tx_http2'] = ['groups' => ['pages']]; +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 490682e..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Tests - - - - - Classes/ - - - - - - - - - -