Skip to content
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

Add FetchMetadata service and a placeholder #369

Merged
merged 2 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Formatter\Placeholders;

use MichalSpacekCz\Http\FetchMetadata\FetchMetadata;
use MichalSpacekCz\Http\FetchMetadata\FetchMetadataHeader;
use Override;

/**
* Inserts live `Sec-Fetch-*` (fetch metadata) headers into blog posts for example.
*
* All headers:
* /---
* **FETCH_METADATA:all**
* \---
* Single header:
* /---
* **FETCH_METADATA:Sec-Fetch-Dest**
* \---
* Inline values:
* ''**FETCH_METADATA:Sec-Fetch-Dest**''
*/
readonly class FetchMetadataTexyFormatterPlaceholder implements TexyFormatterPlaceholder
{

public function __construct(
private FetchMetadata $fetchMetadata,
) {
}


#[Override]
public static function getId(): string
{
return 'FETCH_METADATA';
}


#[Override]
public function replace(string $value): string
{
if ($value === 'all') {
$headers = $this->fetchMetadata->getAllHeaders();
} else {
$header = FetchMetadataHeader::from($value);
$headers = [$header->value => $this->fetchMetadata->getHeader($header)];
}
$result = [];
foreach ($headers as $header => $value) {
$result[] = "{$header}: {$value}";
}
return implode("\n", $result);
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Formatter;
namespace MichalSpacekCz\Formatter\Placeholders;

interface TexyFormatterPlaceholder
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Formatter;
namespace MichalSpacekCz\Formatter\Placeholders;

use Contributte\Translation\Translator;
use MichalSpacekCz\DateTime\DateTimeFormatter;
Expand Down
1 change: 1 addition & 0 deletions site/app/Formatter/TexyFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Contributte\Translation\Exceptions\InvalidArgument;
use Contributte\Translation\Translator;
use MichalSpacekCz\Formatter\Placeholders\TexyFormatterPlaceholder;
use MichalSpacekCz\Utils\Hash;
use Nette\Utils\Html;
use Nette\Utils\Strings;
Expand Down
1 change: 1 addition & 0 deletions site/app/Formatter/TexyPhraseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use MichalSpacekCz\Application\Locale\LocaleLinkGenerator;
use MichalSpacekCz\Articles\Blog\BlogPostLocaleUrls;
use MichalSpacekCz\Formatter\Exceptions\UnexpectedHandlerInvocationReturnType;
use MichalSpacekCz\Formatter\Placeholders\TrainingDateTexyFormatterPlaceholder;
use MichalSpacekCz\ShouldNotHappenException;
use MichalSpacekCz\Training\TrainingLocales;
use Nette\Application\Application;
Expand Down
35 changes: 35 additions & 0 deletions site/app/Http/FetchMetadata/FetchMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\FetchMetadata;

use Nette\Http\IRequest;

readonly class FetchMetadata
{

public function __construct(
private IRequest $httpRequest,
) {
}


public function getHeader(FetchMetadataHeader $header): ?string
{
return $this->httpRequest->getHeader($header->value);
}


/**
* @return array<value-of<FetchMetadataHeader>, string|null>
*/
public function getAllHeaders(): array
{
$headers = [];
foreach (FetchMetadataHeader::cases() as $header) {
$headers[$header->value] = $this->getHeader($header);
}
return $headers;
}

}
14 changes: 14 additions & 0 deletions site/app/Http/FetchMetadata/FetchMetadataHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\FetchMetadata;

enum FetchMetadataHeader: string
{

case Dest = 'Sec-Fetch-Dest';
case Mode = 'Sec-Fetch-Mode';
case Site = 'Sec-Fetch-Site';
case User = 'Sec-Fetch-User';

}
2 changes: 1 addition & 1 deletion site/app/Test/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public function setMethod(string $method): void

public function setHeader(string $name, string $value): void
{
$this->headers[$name] = $value;
$this->headers[strtolower($name)] = $value;
}


Expand Down
6 changes: 4 additions & 2 deletions site/config/services.neon
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,17 @@ services:
- MichalSpacekCz\Form\TrainingReviewFormFactory
- MichalSpacekCz\Form\UnprotectedFormFactory
- MichalSpacekCz\Form\UpcKeysSsidFormFactory
- MichalSpacekCz\Formatter\TexyFormatter(cache: @texyFormatterPhpFilesAdapter, placeholders: typed(MichalSpacekCz\Formatter\TexyFormatterPlaceholder), allowedLongWords: %texyFormatter.allowed.longWords%, staticRoot: %domain.sharedStaticRoot%, imagesRoot: %domain.imagesRoot%, locationRoot: %domain.locationRoot%)
- MichalSpacekCz\Formatter\Placeholders\TrainingDateTexyFormatterPlaceholder
- MichalSpacekCz\Formatter\Placeholders\FetchMetadataTexyFormatterPlaceholder
- MichalSpacekCz\Formatter\TexyFormatter(cache: @texyFormatterPhpFilesAdapter, placeholders: typed(MichalSpacekCz\Formatter\Placeholders\TexyFormatterPlaceholder), allowedLongWords: %texyFormatter.allowed.longWords%, staticRoot: %domain.sharedStaticRoot%, imagesRoot: %domain.imagesRoot%, locationRoot: %domain.locationRoot%)
texyFormatterNoPlaceholders:
create: MichalSpacekCz\Formatter\TexyFormatter(cache: @texyFormatterPhpFilesAdapter, placeholders: [], allowedLongWords: %texyFormatter.allowed.longWords%, staticRoot: %domain.sharedStaticRoot%, imagesRoot: %domain.imagesRoot%, locationRoot: %domain.locationRoot%)
autowired: false
- MichalSpacekCz\Formatter\TexyPhraseHandler
- MichalSpacekCz\Formatter\TrainingDateTexyFormatterPlaceholder
httpClient: MichalSpacekCz\Http\Client\HttpClient
- MichalSpacekCz\Http\Cookies\CookieDescriptions
- MichalSpacekCz\Http\Cookies\Cookies
- MichalSpacekCz\Http\FetchMetadata\FetchMetadata
- MichalSpacekCz\Http\HttpInput
- MichalSpacekCz\Http\Redirections
- MichalSpacekCz\Http\SecurityHeaders(permissionsPolicy: %permissionsPolicy%)
Expand Down
1 change: 1 addition & 0 deletions site/psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<errorLevel type="suppress">
<referencedClass name="*Presenter" />
<referencedClass name="MichalSpacekCz\CompanyInfo\CompanyRegister*" /> <!-- An array of these is passed to MichalSpacekCz\CompanyInfo\CompanyInfo::__construct() by the DIC -->
<referencedClass name="MichalSpacekCz\Formatter\Placeholders\*" /> <!-- An array of these is passed to MichalSpacekCz\Formatter\TexyFormatter::__construct() by the DIC -->
<referencedClass name="MichalSpacekCz\Test\Http\NullSession" /> <!-- Used in tests.neon -->
<referencedClass name="MichalSpacekCz\Tls\CertificateMonitor" /> <!-- Used in bin/certmonitor.php but can't analyze bin because https://github.com/vimeo/psalm/issues/10143 -->
</errorLevel>
Expand Down
10 changes: 7 additions & 3 deletions site/tests/Formatter/TexyFormatterTest.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace MichalSpacekCz\Formatter;
use DateTime;
use MichalSpacekCz\Test\Application\ApplicationPresenter;
use MichalSpacekCz\Test\Database\Database;
use MichalSpacekCz\Test\Http\Request;
use MichalSpacekCz\Test\TestCaseRunner;
use Nette\Application\Application;
use Nette\Utils\Html;
Expand All @@ -32,6 +33,7 @@ class TexyFormatterTest extends TestCase
public function __construct(
private readonly TexyFormatter $texyFormatter,
private readonly AdapterInterface $cacheInterface,
Request $httpRequest,
Database $database,
Application $application,
ApplicationPresenter $applicationPresenter,
Expand Down Expand Up @@ -105,21 +107,23 @@ class TexyFormatterTest extends TestCase
'cs_CZ' => 'bezpecnost-php-aplikaci',
'en_US' => 'php-application-security',
]);
$httpRequest->setHeader('Sec-Fetch-Dest', 'iframe');
$this->expectedFormatted = "<strong>foo <a\n"
. "href=\"https://example.com/?dest=%2F%2F%3AWww%3ATrainings%3Atraining&amp;args=bezpecnost-php-aplikaci\">bar</a>\n"
. "<small>(messages.trainings.nextdates: <strong>5.–7. ledna 2020</strong> messages.label.remote, <strong>5.–7. února 2020</strong> Le city 2)</small></strong>";
. "<small>(messages.trainings.nextdates: <strong>5.–7. ledna 2020</strong> messages.label.remote, <strong>5.–7. února 2020</strong> Le city 2)</small></strong>\n"
. "Sec-Fetch-Dest: iframe";
}


public function testFormat(): void
{
Assert::same($this->expectedFormatted, $this->texyFormatter->format('**foo "bar":[training:' . self::TRAINING_ACTION . ']**')->toHtml());
Assert::same($this->expectedFormatted, $this->texyFormatter->format('**foo "bar":[training:' . self::TRAINING_ACTION . "]**\n''**FETCH_METADATA:Sec-Fetch-Dest**''")->toHtml());
}


public function testFormatBlock(): void
{
Assert::same("<p>{$this->expectedFormatted}</p>\n", $this->texyFormatter->formatBlock('**foo "bar":[training:' . self::TRAINING_ACTION . ']**')->toHtml());
Assert::same("<p>{$this->expectedFormatted}</p>\n", $this->texyFormatter->formatBlock('**foo "bar":[training:' . self::TRAINING_ACTION . "]**\n''**FETCH_METADATA:Sec-Fetch-Dest**''")->toHtml());
}


Expand Down
59 changes: 59 additions & 0 deletions site/tests/Http/FetchMetadata/FetchMetadataTest.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\FetchMetadata;

use MichalSpacekCz\Test\Http\Request;
use MichalSpacekCz\Test\TestCaseRunner;
use Tester\Assert;
use Tester\TestCase;

require __DIR__ . '/../../bootstrap.php';

/** @testCase */
class FetchMetadataTest extends TestCase
{

public function __construct(
private readonly Request $httpRequest,
private readonly FetchMetadata $fetchMetadata,
) {
}


public function testGetHeader(): void
{
$this->httpRequest->setHeader(FetchMetadataHeader::Dest->value, 'document');
Assert::same('document', $this->fetchMetadata->getHeader(FetchMetadataHeader::Dest));
Assert::null($this->fetchMetadata->getHeader(FetchMetadataHeader::Site));
}


public function testGetAllHeaders(): void
{
$this->httpRequest->setHeader(FetchMetadataHeader::Dest->value, 'document');
$expectedHeaders = [
'Sec-Fetch-Dest' => 'document',
'Sec-Fetch-Mode' => null,
'Sec-Fetch-Site' => null,
'Sec-Fetch-User' => null,
];
Assert::same($expectedHeaders, $this->fetchMetadata->getAllHeaders());
Assert::null($this->fetchMetadata->getHeader(FetchMetadataHeader::Site));

$this->httpRequest->setHeader(FetchMetadataHeader::Dest->value, 'document');
$this->httpRequest->setHeader(FetchMetadataHeader::Mode->value, 'navigate');
$this->httpRequest->setHeader(FetchMetadataHeader::Site->value, 'cross-site');
$this->httpRequest->setHeader(FetchMetadataHeader::User->value, '?1');
$expectedHeaders = [
'Sec-Fetch-Dest' => 'document',
'Sec-Fetch-Mode' => 'navigate',
'Sec-Fetch-Site' => 'cross-site',
'Sec-Fetch-User' => '?1',
];
Assert::same($expectedHeaders, $this->fetchMetadata->getAllHeaders());
}

}

TestCaseRunner::run(FetchMetadataTest::class);