-
Notifications
You must be signed in to change notification settings - Fork 4
Release/api key verification middleware #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
9001ea3
[FEATURE] Serve IndexNow API key verification file automatically
e1b5660
Merge PR #30: [FEATURE] Serve IndexNow API key verification file auto…
hojalatheef 4b9367f
[BUGFIX] Handle site-specific paths and improve API key handling
hojalatheef fb2427a
[BUGFIX] Simplify namespace usage in middleware configuration
hojalatheef be2ddff
[RELEASE] Mark extension as stable and update to version 1.0.0
hojalatheef 5ac5761
[TEST] Add functional tests for ApiKeyVerificationMiddleware
hojalatheef beeb89a
[TASK] Add license header to `RequestMiddlewares` and fix file format…
hojalatheef 5641ceb
Update Classes/Middleware/ApiKeyVerificationMiddleware.php
hojalatheef a5be975
Update Tests/Functional/Middleware/ApiKeyVerificationMiddlewareTest.php
hojalatheef ef06e89
[REFACTOR] Remove functional tests and replace API key handling using…
hojalatheef 0164ec8
[REFACTOR] Enforce strict types and update middleware configuration
hojalatheef cfe8d2e
[TEST] Add unit tests for `ApiKeyVerificationMiddleware`
hojalatheef 1a3a6a3
[FEATURE] Add default configuration and localization for IndexNow ext…
hojalatheef 6c0f300
[TASK] Add new author and update extension version to 0.0.10
hojalatheef b9cc3a3
[BUGFIX] Add missing newline at EOF in ApiKeyVerificationMiddlewareTest
hojalatheef 786b735
[TASK] Update CI workflow to include unit tests and expand functional…
hojalatheef 61630e5
[REFACTOR] Remove `ApiKeyVerificationMiddlewareTest` and associated t…
hojalatheef File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the package jweiland/indexnow. | ||
| * | ||
| * For the full copyright and license information, please read the | ||
| * LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace JWeiland\IndexNow\Middleware; | ||
|
|
||
| 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\HtmlResponse; | ||
| use TYPO3\CMS\Core\Site\Entity\SiteInterface; | ||
|
|
||
| /** | ||
| * Serves the IndexNow API key verification file at /{apiKey}.txt | ||
| * | ||
| * IndexNow requires a verification file at the root of the website | ||
| * whose filename and content match the configured API key. This | ||
| * 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 | ||
| */ | ||
| final readonly class ApiKeyVerificationMiddleware implements MiddlewareInterface | ||
| { | ||
| public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface | ||
| { | ||
| $site = $request->getAttribute('site'); | ||
| if (!$site instanceof SiteInterface) { | ||
| return $handler->handle($request); | ||
| } | ||
|
|
||
| $apiKey = (string)$site->getSettings()->get('indexnow.apiKey', ''); | ||
| if ($apiKey === '') { | ||
| 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); | ||
| } | ||
|
|
||
| return new HtmlResponse( | ||
| $apiKey, | ||
| 200, | ||
| [ | ||
| 'Content-Type' => 'text/plain; charset=utf-8', | ||
| 'Content-Length' => (string)strlen($apiKey), | ||
| 'Cache-Control' => 'public, max-age=86400', | ||
| ], | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * This file is part of the package jweiland/indexnow. | ||
| * | ||
| * For the full copyright and license information, please read the | ||
| * LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| use JWeiland\IndexNow\Middleware\ApiKeyVerificationMiddleware; | ||
|
|
||
| return [ | ||
| 'frontend' => [ | ||
| 'jweiland/indexnow/api-key-verification' => [ | ||
| 'target' => ApiKeyVerificationMiddleware::class, | ||
| 'after' => [ | ||
| 'typo3/cms-frontend/site', | ||
| ], | ||
| ], | ||
| ], | ||
| ]; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| name: jweiland/indexnow-default | ||
| label: 'IndexNow - Default Set' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: '' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?xml version="1.0" encoding="utf-8" standalone="yes"?> | ||
| <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> | ||
| <file source-language="en" datatype="plaintext" original="messages"> | ||
| <body> | ||
| <trans-unit id="settings.indexnow.apiKey.label"> | ||
| <source>IndexNow API Key</source> | ||
| </trans-unit> | ||
| <trans-unit id="settings.indexnow.apiKey.description"> | ||
| <source>Per-site API key for the IndexNow search engine verification endpoint. Obtain a key at https://www.indexnow.org/.</source> | ||
| </trans-unit> | ||
| </body> | ||
| </file> | ||
| </xliff> |
154 changes: 154 additions & 0 deletions
154
Tests/Unit/Middleware/ApiKeyVerificationMiddlewareTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of the package jweiland/indexnow. | ||
| * | ||
| * For the full copyright and license information, please read the | ||
| * LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace JWeiland\IndexNow\Tests\Unit\Middleware; | ||
|
|
||
| use JWeiland\IndexNow\Middleware\ApiKeyVerificationMiddleware; | ||
| use PHPUnit\Framework\Attributes\Test; | ||
| use PHPUnit\Framework\MockObject\MockObject; | ||
| use Psr\Http\Message\ResponseInterface; | ||
| use Psr\Http\Server\RequestHandlerInterface; | ||
| use TYPO3\CMS\Core\Http\ServerRequest; | ||
| use TYPO3\CMS\Core\Site\Entity\Site; | ||
| use TYPO3\CMS\Core\Site\Entity\SiteSettings; | ||
| use TYPO3\TestingFramework\Core\Unit\UnitTestCase; | ||
|
|
||
| class ApiKeyVerificationMiddlewareTest extends UnitTestCase | ||
| { | ||
| private ApiKeyVerificationMiddleware $subject; | ||
|
|
||
| private RequestHandlerInterface&MockObject $handlerMock; | ||
|
|
||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
|
|
||
| $this->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()); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.