From 9001ea38c4a2f58fb2932dda59c979d028583c1c Mon Sep 17 00:00:00 2001 From: Ingo Hollmann Date: Thu, 26 Mar 2026 16:30:11 +0100 Subject: [PATCH 01/16] [FEATURE] Serve IndexNow API key verification file automatically IndexNow search engines verify ownership by requesting a text file at /{apiKey}.txt whose content matches the configured API key. Instead of requiring users to manually create and maintain a static file, this PSR-15 middleware serves it dynamically from the extension configuration. Once the API key is saved in extension settings, the verification file is immediately available. The middleware runs early in the frontend pipeline (before site resolution) and only matches root-level .txt requests where the filename equals the configured key. All other requests pass through unchanged. Responses include a 24-hour cache header. Also updates README to reflect that manual file creation is no longer needed. --- .../ApiKeyVerificationMiddleware.php | 67 +++++++++++++++++++ Configuration/RequestMiddlewares.php | 15 +++++ README.md | 38 ++++++----- 3 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 Classes/Middleware/ApiKeyVerificationMiddleware.php create mode 100644 Configuration/RequestMiddlewares.php diff --git a/Classes/Middleware/ApiKeyVerificationMiddleware.php b/Classes/Middleware/ApiKeyVerificationMiddleware.php new file mode 100644 index 0000000..153e282 --- /dev/null +++ b/Classes/Middleware/ApiKeyVerificationMiddleware.php @@ -0,0 +1,67 @@ +getUri()->getPath(), '/'); + + // Quick exit: only handle .txt files at root level (no slashes in path) + if (!str_ends_with($path, '.txt') || str_contains($path, '/')) { + return $handler->handle($request); + } + + try { + $apiKey = $this->extConf->getApiKey(); + } catch (\Exception) { + // No API key configured — skip + return $handler->handle($request); + } + + if ($path !== $apiKey . '.txt') { + return $handler->handle($request); + } + + $stream = new Stream('php://temp', 'rw'); + $stream->write($apiKey); + $stream->rewind(); + + return new Response($stream, 200, [ + 'Content-Type' => 'text/plain; charset=utf-8', + 'Cache-Control' => 'public, max-age=86400', + ]); + } +} diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php new file mode 100644 index 0000000..6d0631e --- /dev/null +++ b/Configuration/RequestMiddlewares.php @@ -0,0 +1,15 @@ + [ + 'jweiland/indexnow/api-key-verification' => [ + 'target' => \JWeiland\IndexNow\Middleware\ApiKeyVerificationMiddleware::class, + 'after' => [ + 'typo3/cms-core/normalized-params-attribute', + ], + 'before' => [ + 'typo3/cms-frontend/site', + ], + ], + ], +]; diff --git a/README.md b/README.md index f781d30..872cbed 100644 --- a/README.md +++ b/README.md @@ -77,18 +77,21 @@ engines. Notify batch mode : If enabled, the search engine will be notified using batch mode. This means that modified URLs are sent in a single request. A maximum of 10,000 URLs can be sent per batch -### Host API Key file +### API Key Verification File -Create a file named `[API key].txt` with your API key as content and move it -into your document root directory of your website server. +IndexNow search engines [verify ownership](https://www.indexnow.org/documentation) +by requesting a text file at `https://example.com/{apiKey}.txt` whose content +matches the API key. -#### Example +This extension **serves the verification file automatically** from the +configured API key — no manual file creation needed. Once you save the API key +in the extension settings, the file is immediately available at +`https://your-site.com/{apiKey}.txt`. -If you chose `abc-ABC-123` as your API key you have to create a file named -`abc-ABC-123.txt` and set `abc-ABC-123` as content of that file. Upload file -`abc-ABC-123.txt` into the `/var/www/my-typo3-page/public` folder. Open -`https://example.com/abc-ABC-123.txt` to make sure the file is public -available and its content is `abc-ABC-123`. +> **Note:** The verification file is served by a PSR-15 middleware that runs +> early in the request pipeline. It only responds to root-level `.txt` +> requests matching the configured key. All other requests pass through +> unchanged. ### Task @@ -131,14 +134,13 @@ var/log/typo3_indexnow_[hash].log ### IndexNow was informed, but search results are not updated -The IndexNow provider will use your API key and request the file: +The IndexNow provider will verify your API key by requesting +`https://your-site.com/{apiKey}.txt`. This file is served automatically +by the extension. If verification still fails, check: -```text -[API key].txt -``` - -with API key as content from your server. If it does not exist, validation -fails and search engines will provide updated information much later. +1. The API key is set in extension settings (`Settings` → `Configure extensions` → `indexnow`) +2. The file is accessible: open `https://your-site.com/{apiKey}.txt` in a browser +3. No caching proxy or WAF is blocking the `.txt` file request ### I have changed content, but there is no record in `tx_indexnow_stack` @@ -205,8 +207,8 @@ Creating a new page or content element should call the IndexNow services. Currently, only modified pages and content elements will be processed. We should use the afterAllDatabaseOperations DataHandler hook instead. -Nice to have: Add a section into EXT:reports, if file with API key is -available. +Nice to have: Add a section into EXT:reports to check IndexNow +configuration health. ## Support From 4b9367f0b228ef4f56f88263903a791973f36d14 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Fri, 29 May 2026 02:12:16 +0200 Subject: [PATCH 02/16] [BUGFIX] Handle site-specific paths and improve API key handling - Added support for resolving site-specific paths in the API key verification middleware. - Replaced generic exception handling with `ApiKeyNotAvailableException`. - Enhanced response headers to include `Content-Length` for better caching. --- .../Middleware/ApiKeyVerificationMiddleware.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Classes/Middleware/ApiKeyVerificationMiddleware.php b/Classes/Middleware/ApiKeyVerificationMiddleware.php index 153e282..31dd9d1 100644 --- a/Classes/Middleware/ApiKeyVerificationMiddleware.php +++ b/Classes/Middleware/ApiKeyVerificationMiddleware.php @@ -11,6 +11,7 @@ namespace JWeiland\IndexNow\Middleware; +use JWeiland\IndexNow\Configuration\Exception\ApiKeyNotAvailableException; use JWeiland\IndexNow\Configuration\ExtConf; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -18,6 +19,7 @@ use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Http\Response; use TYPO3\CMS\Core\Http\Stream; +use TYPO3\CMS\Core\Site\Entity\Site; /** * Serves the IndexNow API key verification file at /{apiKey}.txt @@ -39,6 +41,14 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $path = ltrim($request->getUri()->getPath(), '/'); + $site = $request->getAttribute('site'); + if ($site instanceof Site) { + $sitePath = ltrim($site->getBase()->getPath(), '/'); + if ($sitePath !== '' && str_starts_with($path, $sitePath)) { + $path = ltrim(substr($path, strlen($sitePath)), '/'); + } + } + // Quick exit: only handle .txt files at root level (no slashes in path) if (!str_ends_with($path, '.txt') || str_contains($path, '/')) { return $handler->handle($request); @@ -46,8 +56,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface try { $apiKey = $this->extConf->getApiKey(); - } catch (\Exception) { - // No API key configured — skip + } catch (ApiKeyNotAvailableException) { return $handler->handle($request); } @@ -61,6 +70,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return new Response($stream, 200, [ 'Content-Type' => 'text/plain; charset=utf-8', + 'Content-Length' => (string)strlen($apiKey), 'Cache-Control' => 'public, max-age=86400', ]); } From fb2427a38c8513342481d1a5050c8303783f62f4 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Fri, 29 May 2026 02:12:27 +0200 Subject: [PATCH 03/16] [BUGFIX] Simplify namespace usage in middleware configuration --- Configuration/RequestMiddlewares.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index 6d0631e..d05ada9 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -1,9 +1,11 @@ [ 'jweiland/indexnow/api-key-verification' => [ - 'target' => \JWeiland\IndexNow\Middleware\ApiKeyVerificationMiddleware::class, + 'target' => ApiKeyVerificationMiddleware::class, 'after' => [ 'typo3/cms-core/normalized-params-attribute', ], From be2ddff30fc72940f5ddc0dddcc869b68cd88a98 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Fri, 29 May 2026 02:12:58 +0200 Subject: [PATCH 04/16] [RELEASE] Mark extension as stable and update to version 1.0.0 --- ext_emconf.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext_emconf.php b/ext_emconf.php index 9f02632..62c5eff 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -12,9 +12,9 @@ 'description' => 'TYPO3 extension to inform various search engines over IndexNow endpoint about content updates', 'category' => 'service', 'author' => 'Stefan Froemken', - 'author_email' => 'sfroemken@jweiland.net', - 'state' => 'experimental', - 'version' => '0.0.9', + 'author_email' => 'projects@jweiland.net', + 'state' => 'stable', + 'version' => '1.0.0', 'constraints' => [ 'depends' => [ 'typo3' => '12.4.31-13.4.99', From 5ac576164bf97735edb3294983892cb0e4b6352f Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Fri, 29 May 2026 02:13:18 +0200 Subject: [PATCH 05/16] [TEST] Add functional tests for ApiKeyVerificationMiddleware --- .../ApiKeyVerificationMiddlewareTest.php | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php diff --git a/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php b/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php new file mode 100644 index 0000000..b9f7209 --- /dev/null +++ b/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php @@ -0,0 +1,125 @@ +extConfMock = $this->createMock(ExtConf::class); + $this->handlerMock = $this->createMock(RequestHandlerInterface::class); + $this->subject = new ApiKeyVerificationMiddleware($this->extConfMock); + } + + protected function tearDown(): void + { + unset( + $this->subject, + $this->extConfMock, + $this->handlerMock, + ); + + parent::tearDown(); + } + + #[Test] + public function processPassesThroughNonTxtRequest(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + $this->extConfMock->expects(self::never())->method('getApiKey'); + + $request = new ServerRequest('https://example.com/some-page', 'GET'); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughSubdirectoryTxtRequest(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + $this->extConfMock->expects(self::never())->method('getApiKey'); + + $request = new ServerRequest('https://example.com/subdir/key.txt', 'GET'); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughWhenNoApiKeyConfigured(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + $this->extConfMock->expects(self::once())->method('getApiKey') + ->willThrowException(new ApiKeyNotAvailableException('No API key configured', 1636752398)); + + $request = new ServerRequest('https://example.com/some-key.txt', 'GET'); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughForNonMatchingTxtFile(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + $this->extConfMock->expects(self::once())->method('getApiKey')->willReturn('my-api-key'); + + $request = new ServerRequest('https://example.com/other-file.txt', 'GET'); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processReturnsApiKeyFileForMatchingRequest(): void + { + $this->handlerMock->expects(self::never())->method('handle'); + $this->extConfMock->expects(self::once())->method('getApiKey')->willReturn('my-api-key'); + + $request = new ServerRequest('https://example.com/my-api-key.txt', 'GET'); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); + self::assertSame('public, max-age=86400', $response->getHeaderLine('Cache-Control')); + self::assertSame('10', $response->getHeaderLine('Content-Length')); + self::assertSame('my-api-key', (string)$response->getBody()); + } +} \ No newline at end of file From beeb89af7dc52ae89a6d3ff2dea4ac5d486914d0 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Fri, 29 May 2026 02:15:59 +0200 Subject: [PATCH 06/16] [TASK] Add license header to `RequestMiddlewares` and fix file formatting --- Configuration/RequestMiddlewares.php | 7 +++++++ .../Middleware/ApiKeyVerificationMiddlewareTest.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index d05ada9..f361a9e 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -1,5 +1,12 @@ getHeaderLine('Content-Length')); self::assertSame('my-api-key', (string)$response->getBody()); } -} \ No newline at end of file +} From 5641ceb9eafde14f28923938ccc1eebff26ea2fe Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef <144037456+hojalatheef@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:31:58 +0200 Subject: [PATCH 07/16] Update Classes/Middleware/ApiKeyVerificationMiddleware.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated Documentation Block Co-authored-by: Stefan Frömken <123929835+sfroemkenjw@users.noreply.github.com> --- Classes/Middleware/ApiKeyVerificationMiddleware.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Middleware/ApiKeyVerificationMiddleware.php b/Classes/Middleware/ApiKeyVerificationMiddleware.php index 31dd9d1..3fc589d 100644 --- a/Classes/Middleware/ApiKeyVerificationMiddleware.php +++ b/Classes/Middleware/ApiKeyVerificationMiddleware.php @@ -26,7 +26,8 @@ * * IndexNow requires a verification file at the root of the website * whose filename and content match the configured API key. This - * middleware serves that file dynamically from the extension configuration, + * middleware dynamically serves that endpoint, using the API key + * stored in the extension configuration, * removing the need to manually create or update a static file. * * @see https://www.indexnow.org/documentation From a5be975c219abcfce7f06b88e3f4d688d05af718 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef <144037456+hojalatheef@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:33:33 +0200 Subject: [PATCH 08/16] Update Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stefan Frömken <123929835+sfroemkenjw@users.noreply.github.com> --- .../Functional/Middleware/ApiKeyVerificationMiddlewareTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php b/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php index daf52a7..762a7ad 100644 --- a/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php +++ b/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php @@ -32,7 +32,7 @@ class ApiKeyVerificationMiddlewareTest extends FunctionalTestCase private ExtConf|MockObject $extConfMock; - private RequestHandlerInterface|MockObject $handlerMock; + private RequestHandlerInterface&MockObject $handlerMock; protected function setUp(): void { From ef06e892c35d6b0b06e75929757557fcabc5a6bd Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:27:00 +0200 Subject: [PATCH 09/16] [REFACTOR] Remove functional tests and replace API key handling using site settings - Deleted `ApiKeyVerificationMiddlewareTest` along with its test cases. - Updated `ApiKeyVerificationMiddleware` to use site settings for retrieving the API key instead of the `ExtConf` class. - Streamlined path extraction and request handling for better maintainability. --- .../ApiKeyVerificationMiddleware.php | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/Classes/Middleware/ApiKeyVerificationMiddleware.php b/Classes/Middleware/ApiKeyVerificationMiddleware.php index 3fc589d..367ec4c 100644 --- a/Classes/Middleware/ApiKeyVerificationMiddleware.php +++ b/Classes/Middleware/ApiKeyVerificationMiddleware.php @@ -11,15 +11,12 @@ namespace JWeiland\IndexNow\Middleware; -use JWeiland\IndexNow\Configuration\Exception\ApiKeyNotAvailableException; -use JWeiland\IndexNow\Configuration\ExtConf; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use TYPO3\CMS\Core\Http\Response; -use TYPO3\CMS\Core\Http\Stream; -use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Http\HtmlResponse; +use TYPO3\CMS\Core\Site\Entity\SiteInterface; /** * Serves the IndexNow API key verification file at /{apiKey}.txt @@ -32,47 +29,40 @@ * * @see https://www.indexnow.org/documentation */ -class ApiKeyVerificationMiddleware implements MiddlewareInterface +final readonly class ApiKeyVerificationMiddleware implements MiddlewareInterface { - public function __construct( - private readonly ExtConf $extConf, - ) {} - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $path = ltrim($request->getUri()->getPath(), '/'); - $site = $request->getAttribute('site'); - if ($site instanceof Site) { - $sitePath = ltrim($site->getBase()->getPath(), '/'); - if ($sitePath !== '' && str_starts_with($path, $sitePath)) { - $path = ltrim(substr($path, strlen($sitePath)), '/'); - } + if (!$site instanceof SiteInterface) { + return $handler->handle($request); } - // Quick exit: only handle .txt files at root level (no slashes in path) - if (!str_ends_with($path, '.txt') || str_contains($path, '/')) { + $apiKey = (string)$site->getSettings()->get('indexnow.apiKey', ''); + if ($apiKey === '') { return $handler->handle($request); } - try { - $apiKey = $this->extConf->getApiKey(); - } catch (ApiKeyNotAvailableException) { - return $handler->handle($request); + $serverParams = $request->getServerParams(); + $requestPath = parse_url($serverParams['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'; + $path = ltrim($requestPath, '/'); + $sitePath = ltrim($site->getBase()->getPath(), '/'); + if ($sitePath !== '' && str_starts_with($path, $sitePath)) { + $path = ltrim(substr($path, strlen($sitePath)), '/'); } if ($path !== $apiKey . '.txt') { return $handler->handle($request); } - $stream = new Stream('php://temp', 'rw'); - $stream->write($apiKey); - $stream->rewind(); - - return new Response($stream, 200, [ - 'Content-Type' => 'text/plain; charset=utf-8', - 'Content-Length' => (string)strlen($apiKey), - 'Cache-Control' => 'public, max-age=86400', - ]); + return new HtmlResponse( + $apiKey, + 200, + [ + 'Content-Type' => 'text/plain; charset=utf-8', + 'Content-Length' => (string)strlen($apiKey), + 'Cache-Control' => 'public, max-age=86400', + ], + ); } } From 0164ec8b8e209e396b447263f5fe697c4325a814 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:27:45 +0200 Subject: [PATCH 10/16] [REFACTOR] Enforce strict types and update middleware configuration - Added strict type declarations to `RequestMiddlewares.php`. - Simplified middleware configuration by removing unnecessary `before` and `after` keys. - Removed unused `apiKey` setting from `ext_conf_template.txt`. --- Configuration/RequestMiddlewares.php | 5 ++--- ext_conf_template.txt | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index f361a9e..736d286 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -1,5 +1,7 @@ [ 'target' => ApiKeyVerificationMiddleware::class, 'after' => [ - 'typo3/cms-core/normalized-params-attribute', - ], - 'before' => [ 'typo3/cms-frontend/site', ], ], diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 2fcecf5..28d71a0 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -1,5 +1,3 @@ -# cat=basic; type=string; label=LLL:EXT:indexnow/Resources/Private/Language/ExtConf.xlf:apiKey -apiKey = # cat=basic; type=string; label=LLL:EXT:indexnow/Resources/Private/Language/ExtConf.xlf:searchEngineEndpoint searchEngineEndpoint = https://www.bing.com/indexnow # cat=basic; type=boolean; label=LLL:EXT:indexnow/Resources/Private/Language/ExtConf.xlf:notifyBatchMode From cfe8d2e05c7172dcf07ffd0203b3c7496f8a76fc Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:28:25 +0200 Subject: [PATCH 11/16] [TEST] Add unit tests for `ApiKeyVerificationMiddleware` --- .../ApiKeyVerificationMiddlewareTest.php | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php diff --git a/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php b/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php new file mode 100644 index 0000000..ce71bcf --- /dev/null +++ b/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php @@ -0,0 +1,154 @@ +handlerMock = $this->createMock(RequestHandlerInterface::class); + $this->subject = new ApiKeyVerificationMiddleware(); + } + + protected function tearDown(): void + { + unset( + $this->subject, + $this->handlerMock, + ); + + parent::tearDown(); + } + + private function makeSite(string $base, string $apiKey): Site + { + return new Site( + 'test', + 1, + ['base' => $base], + SiteSettings::createFromSettingsTree(['indexnow' => ['apiKey' => $apiKey]]), + ); + } + + #[Test] + public function processPassesThroughWhenNoSiteAttributePresent(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + + $request = new ServerRequest('https://example.com/some-key.txt', 'GET', 'php://input', [], ['REQUEST_URI' => '/some-key.txt']); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughWhenNoApiKeyConfigured(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + + $site = new Site('test', 1, ['base' => 'https://example.com/'], SiteSettings::createFromSettingsTree([])); + $request = (new ServerRequest('https://example.com/some-key.txt', 'GET', 'php://input', [], ['REQUEST_URI' => '/some-key.txt'])) + ->withAttribute('site', $site); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughNonTxtRequest(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + + $site = $this->makeSite('https://example.com/', 'my-api-key'); + $request = (new ServerRequest('https://example.com/some-page', 'GET', 'php://input', [], ['REQUEST_URI' => '/some-page'])) + ->withAttribute('site', $site); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughSubdirectoryTxtRequest(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + + $site = $this->makeSite('https://example.com/', 'my-api-key'); + $request = (new ServerRequest('https://example.com/subdir/my-api-key.txt', 'GET', 'php://input', [], ['REQUEST_URI' => '/subdir/my-api-key.txt'])) + ->withAttribute('site', $site); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processPassesThroughForNonMatchingTxtFile(): void + { + $handlerResponse = $this->createMock(ResponseInterface::class); + $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); + + $site = $this->makeSite('https://example.com/', 'my-api-key'); + $request = (new ServerRequest('https://example.com/other-file.txt', 'GET', 'php://input', [], ['REQUEST_URI' => '/other-file.txt'])) + ->withAttribute('site', $site); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame($handlerResponse, $response); + } + + #[Test] + public function processReturnsApiKeyFileForMatchingRequest(): void + { + $this->handlerMock->expects(self::never())->method('handle'); + + $site = $this->makeSite('https://example.com/', 'my-api-key'); + $request = (new ServerRequest('https://example.com/my-api-key.txt', 'GET', 'php://input', [], ['REQUEST_URI' => '/my-api-key.txt'])) + ->withAttribute('site', $site); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); + self::assertSame('public, max-age=86400', $response->getHeaderLine('Cache-Control')); + self::assertSame('10', $response->getHeaderLine('Content-Length')); + self::assertSame('my-api-key', (string)$response->getBody()); + } + + #[Test] + public function processHandlesSiteWithSubdirectoryBase(): void + { + $this->handlerMock->expects(self::never())->method('handle'); + + $site = $this->makeSite('https://example.com/de/', 'my-api-key'); + $request = (new ServerRequest('https://example.com/de/my-api-key.txt', 'GET', 'php://input', [], ['REQUEST_URI' => '/de/my-api-key.txt'])) + ->withAttribute('site', $site); + $response = $this->subject->process($request, $this->handlerMock); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('my-api-key', (string)$response->getBody()); + } +} \ No newline at end of file From 1a3a6a32754c76f84594dada63fd568b99ba9869 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:28:59 +0200 Subject: [PATCH 12/16] [FEATURE] Add default configuration and localization for IndexNow extension - Introduced `config.yaml` for the default IndexNow set. - Added localized labels and descriptions for IndexNow API key configuration. - Defined API key setting with category, type, and default value. --- Configuration/Sets/IndexNow/config.yaml | 2 ++ .../Sets/IndexNow/settings.definitions.yaml | 10 ++++++++++ Resources/Private/Language/locallang_db.xlf | 13 +++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 Configuration/Sets/IndexNow/config.yaml create mode 100644 Configuration/Sets/IndexNow/settings.definitions.yaml create mode 100644 Resources/Private/Language/locallang_db.xlf diff --git a/Configuration/Sets/IndexNow/config.yaml b/Configuration/Sets/IndexNow/config.yaml new file mode 100644 index 0000000..482063e --- /dev/null +++ b/Configuration/Sets/IndexNow/config.yaml @@ -0,0 +1,2 @@ +name: jweiland/indexnow-default +label: 'IndexNow - Default Set' diff --git a/Configuration/Sets/IndexNow/settings.definitions.yaml b/Configuration/Sets/IndexNow/settings.definitions.yaml new file mode 100644 index 0000000..e64f588 --- /dev/null +++ b/Configuration/Sets/IndexNow/settings.definitions.yaml @@ -0,0 +1,10 @@ +categories: + indexnow: + label: 'IndexNow' +settings: + indexnow.apiKey: + label: 'LLL:EXT:indexnow/Resources/Private/Language/locallang_db.xlf:settings.indexnow.apiKey.label' + description: 'LLL:EXT:indexnow/Resources/Private/Language/locallang_db.xlf:settings.indexnow.apiKey.description' + category: 'indexnow' + type: string + default: '' diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf new file mode 100644 index 0000000..1030851 --- /dev/null +++ b/Resources/Private/Language/locallang_db.xlf @@ -0,0 +1,13 @@ + + + + + + IndexNow API Key + + + Per-site API key for the IndexNow search engine verification endpoint. Obtain a key at https://www.indexnow.org/. + + + + From 6c0f300f5965d2e8f36da7531082777280d296fe Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:32:49 +0200 Subject: [PATCH 13/16] [TASK] Add new author and update extension version to 0.0.10 - Added Hoja Mustaffa Abdul Latheef as co-author in `composer.json` and `ext_emconf.php`. - Updated the extension version in `ext_emconf.php` to 0.0.10. --- composer.json | 4 ++++ ext_emconf.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 64a0aa5..f57def4 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,10 @@ { "name": "Stefan Froemken", "email": "sfroemken@jweiland.net" + }, + { + "name": "Hoja Mustaffa Abdul Latheef", + "email": "hlatheef@jweiland.net" } ], "require": { diff --git a/ext_emconf.php b/ext_emconf.php index 62c5eff..1c52eae 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,10 +11,10 @@ 'title' => 'Index now', 'description' => 'TYPO3 extension to inform various search engines over IndexNow endpoint about content updates', 'category' => 'service', - 'author' => 'Stefan Froemken', + 'author' => 'Stefan Froemken, Hoja Mustaffa Abdul Latheef', 'author_email' => 'projects@jweiland.net', 'state' => 'stable', - 'version' => '1.0.0', + 'version' => '0.0.10', 'constraints' => [ 'depends' => [ 'typo3' => '12.4.31-13.4.99', From b9cc3a3e36471527da1acadda6a7ddf2472e07ff Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:37:55 +0200 Subject: [PATCH 14/16] [BUGFIX] Add missing newline at EOF in ApiKeyVerificationMiddlewareTest --- Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php b/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php index ce71bcf..05213c0 100644 --- a/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php +++ b/Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php @@ -151,4 +151,4 @@ public function processHandlesSiteWithSubdirectoryBase(): void self::assertSame(200, $response->getStatusCode()); self::assertSame('my-api-key', (string)$response->getBody()); } -} \ No newline at end of file +} From 786b735ba4fd08c6e533fbf0f88e077eedf5d4de Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:41:12 +0200 Subject: [PATCH 15/16] [TASK] Update CI workflow to include unit tests and expand functional test coverage - Added unit test execution step. - Introduced functional test scenarios for MariaDB, SQLite, and Postgres. --- .github/workflows/ci.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e40622..e5f6bfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,11 +35,14 @@ jobs: - name: 'CGL' run: Build/Scripts/runTests.sh -n -p ${{ matrix.php }} -s cgl - - name: 'Execute functional tests' - run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mysql -s functional + - name: 'Execute unit tests' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s unit - - name: 'Execute functional tests' + - name: 'Execute functional tests with MariaDB' run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d mariadb -s functional - - name: 'Execute functional tests' + - name: 'Execute functional tests with SQLite' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d sqlite -s functional + + - name: 'Execute functional tests with Postgres' run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -d postgres -s functional From 61630e5e24cee187b3f18d47fd39255926139d60 Mon Sep 17 00:00:00 2001 From: Hoja Mustaffa Abdul Latheef Date: Tue, 9 Jun 2026 20:48:41 +0200 Subject: [PATCH 16/16] [REFACTOR] Remove `ApiKeyVerificationMiddlewareTest` and associated test cases - Deleted outdated functional tests for `ApiKeyVerificationMiddleware`. --- .../ApiKeyVerificationMiddlewareTest.php | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php diff --git a/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php b/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php deleted file mode 100644 index 762a7ad..0000000 --- a/Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php +++ /dev/null @@ -1,125 +0,0 @@ -extConfMock = $this->createMock(ExtConf::class); - $this->handlerMock = $this->createMock(RequestHandlerInterface::class); - $this->subject = new ApiKeyVerificationMiddleware($this->extConfMock); - } - - protected function tearDown(): void - { - unset( - $this->subject, - $this->extConfMock, - $this->handlerMock, - ); - - parent::tearDown(); - } - - #[Test] - public function processPassesThroughNonTxtRequest(): void - { - $handlerResponse = $this->createMock(ResponseInterface::class); - $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); - $this->extConfMock->expects(self::never())->method('getApiKey'); - - $request = new ServerRequest('https://example.com/some-page', 'GET'); - $response = $this->subject->process($request, $this->handlerMock); - - self::assertSame($handlerResponse, $response); - } - - #[Test] - public function processPassesThroughSubdirectoryTxtRequest(): void - { - $handlerResponse = $this->createMock(ResponseInterface::class); - $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); - $this->extConfMock->expects(self::never())->method('getApiKey'); - - $request = new ServerRequest('https://example.com/subdir/key.txt', 'GET'); - $response = $this->subject->process($request, $this->handlerMock); - - self::assertSame($handlerResponse, $response); - } - - #[Test] - public function processPassesThroughWhenNoApiKeyConfigured(): void - { - $handlerResponse = $this->createMock(ResponseInterface::class); - $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); - $this->extConfMock->expects(self::once())->method('getApiKey') - ->willThrowException(new ApiKeyNotAvailableException('No API key configured', 1636752398)); - - $request = new ServerRequest('https://example.com/some-key.txt', 'GET'); - $response = $this->subject->process($request, $this->handlerMock); - - self::assertSame($handlerResponse, $response); - } - - #[Test] - public function processPassesThroughForNonMatchingTxtFile(): void - { - $handlerResponse = $this->createMock(ResponseInterface::class); - $this->handlerMock->expects(self::once())->method('handle')->willReturn($handlerResponse); - $this->extConfMock->expects(self::once())->method('getApiKey')->willReturn('my-api-key'); - - $request = new ServerRequest('https://example.com/other-file.txt', 'GET'); - $response = $this->subject->process($request, $this->handlerMock); - - self::assertSame($handlerResponse, $response); - } - - #[Test] - public function processReturnsApiKeyFileForMatchingRequest(): void - { - $this->handlerMock->expects(self::never())->method('handle'); - $this->extConfMock->expects(self::once())->method('getApiKey')->willReturn('my-api-key'); - - $request = new ServerRequest('https://example.com/my-api-key.txt', 'GET'); - $response = $this->subject->process($request, $this->handlerMock); - - self::assertSame(200, $response->getStatusCode()); - self::assertSame('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); - self::assertSame('public, max-age=86400', $response->getHeaderLine('Cache-Control')); - self::assertSame('10', $response->getHeaderLine('Content-Length')); - self::assertSame('my-api-key', (string)$response->getBody()); - } -}