Skip to content

Commit

Permalink
Merge branch '2.5' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Nov 28, 2020
2 parents f99cc0a + 4bac128 commit 544e5c7
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 16 deletions.
27 changes: 26 additions & 1 deletion CHANGELOG.md
Expand Up @@ -20,6 +20,32 @@
* OpenAPI: **BC** Replace all characters other than `[a-zA-Z0-9\.\-_]` to `.` in definition names to be compliant with OpenAPI 3.0 (#3669)
* Add stateless ApiResource attribute

## 2.5.8

For compatibility reasons with Symfony 5.2 and PHP 8, we do not test these legacy packages nor their integration anymore:
- NelmioApiDoc
- FOSUserBundle

* PHP 8 support (#3791, #3745, #3855)
* Metadata: Fix merging null values from annotations (#3711)
* JsonLd: missing @type from collection using output DTOs. (#3699)
* Cache: Improve PurgeHttpCacheListener performances (#3743)
* Cache: Fix VarnishPurger max header length (#3843)
* Identifiers: Do not denormalize the same identifier twice (#3762)
* OpenAPI: Lazy load SwaggerCommand (#3802)
* OpenAPI: Use Output class name instead of the Resource short name when available (#3741)
* Router: replace baseurl only once (#3776)
* Mercure: Publisher bug fixes (#3790, #3739)
* Tests: Github action with windows, now uses setup-php (#3716 #3717, #3814)
* Tests: improve overall PHPUnit and Symfony 5.2 compatibility (#3702 #3700, #3781, #3779, #3822)
* Tests: improve behat compatibility (#3792, #3793)
* Tests: improve JSON Schema assertions (#3807, #3803, #3804, #3806, #3817, #3829, #3830)
* Tests: allow extra options in ApiTestClient (#3486)
* Tests: Use `static_lambda` code style rule (#3725)
* Remove legacy dependencies (#3794)
* Serializer: Catch NotNormalizableValueException to UnexpectedValueEception with inputs (#3697)
* Doctrine: ODM escape search terms in RegexFilter

## 2.5.7

* Compatibility with Symfony 5.1 (#3589 and #3688)
Expand All @@ -38,7 +64,6 @@
* Docs:Upgrade ReDoc to version 2.0.0-rc.40 (#3693)
* Docs:Upgrade GraphiQL to version 1.0.3 (#3693)
* Docs:Upgrade GraphQL Playground to version 1.7.23 (#3693)
>>>>>>> 2.5

## 2.5.6

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -24,7 +24,7 @@
"symfony/property-info": "^3.4 || ^4.4 || ^5.1",
"symfony/serializer": "^4.4 || ^5.1",
"symfony/web-link": "^4.4 || ^5.1",
"willdurand/negotiation": "^2.0.3 || 3.0.x-dev"
"willdurand/negotiation": "^2.0.3 || ^3.0"
},
"require-dev": {
"behat/behat": "^3.1",
Expand Down
38 changes: 36 additions & 2 deletions src/HttpCache/VarnishPurger.php
Expand Up @@ -24,13 +24,15 @@
*/
final class VarnishPurger implements PurgerInterface
{
private const DEFAULT_VARNISH_MAX_HEADER_LENGTH = 8000;

private $clients;
private $maxHeaderLength;

/**
* @param ClientInterface[] $clients
*/
public function __construct(array $clients, int $maxHeaderLength = 7500)
public function __construct(array $clients, int $maxHeaderLength = self::DEFAULT_VARNISH_MAX_HEADER_LENGTH)
{
$this->clients = $clients;
$this->maxHeaderLength = $maxHeaderLength;
Expand Down Expand Up @@ -86,10 +88,42 @@ private function purgeRequest(array $iris)
return sprintf('(^|\,)%s($|\,)', preg_quote($iri));
}, $iris);

$regex = \count($parts) > 1 ? sprintf('(%s)', implode(')|(', $parts)) : array_shift($parts);
foreach ($this->chunkRegexParts($parts) as $regex) {
$this->banRegex($regex);
}
}

private function banRegex(string $regex): void
{
foreach ($this->clients as $client) {
$client->request('BAN', '', ['headers' => ['ApiPlatform-Ban-Regex' => $regex]]);
}
}

private function chunkRegexParts(array $parts): iterable
{
if (1 === \count($parts)) {
yield $parts[0];

return;
}

$concatenatedParts = sprintf('(%s)', implode(")\n(", $parts));

if (\strlen($concatenatedParts) <= $this->maxHeaderLength) {
yield str_replace("\n", '|', $concatenatedParts);

return;
}

$lastSeparator = strrpos(substr($concatenatedParts, 0, $this->maxHeaderLength + 1), "\n");

$chunk = substr($concatenatedParts, 0, $lastSeparator);

yield str_replace("\n", '|', $chunk);

$nextParts = \array_slice($parts, substr_count($chunk, "\n") + 1);

yield from $this->chunkRegexParts($nextParts);
}
}
12 changes: 0 additions & 12 deletions tests/Annotation/ApiResourceTest.php
Expand Up @@ -242,18 +242,6 @@ public function testApiResourceAnnotation()
], $resource->attributes);
}

public function testConstructWithInvalidAttribute()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown property "invalidAttribute" on annotation "ApiPlatform\\Core\\Annotation\\ApiResource".');

new ApiResource([
'shortName' => 'shortName',
'routePrefix' => '/foo',
'invalidAttribute' => 'exception',
]);
}

/**
* @group legacy
* @expectedDeprecation Attribute "accessControl" is deprecated in annotation since API Platform 2.5, prefer using "security" attribute instead
Expand Down
101 changes: 101 additions & 0 deletions tests/HttpCache/VarnishPurgerTest.php
Expand Up @@ -16,8 +16,12 @@
use ApiPlatform\Core\HttpCache\VarnishPurger;
use ApiPlatform\Core\Tests\ProphecyTrait;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response;
use LogicException;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand Down Expand Up @@ -60,4 +64,101 @@ public function testEmptyTags()
$purger = new VarnishPurger([$clientProphecy1->reveal()]);
$purger->purge([]);
}

/**
* @dataProvider provideChunkHeaderCases
*/
public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $regexesToSend)
{
$client = new class() implements ClientInterface {
public $sentRegexes = [];

public function send(RequestInterface $request, array $options = []): ResponseInterface
{
throw new LogicException('Not implemented');
}

public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
{
throw new LogicException('Not implemented');
}

public function request($method, $uri, array $options = []): ResponseInterface
{
$this->sentRegexes[] = $options['headers']['ApiPlatform-Ban-Regex'];

return new Response();
}

public function requestAsync($method, $uri, array $options = []): PromiseInterface
{
throw new LogicException('Not implemented');
}

public function getConfig($option = null)
{
throw new LogicException('Not implemented');
}
};

$purger = new VarnishPurger([$client], $maxHeaderLength);
$purger->purge($iris);

self::assertSame($regexesToSend, $client->sentRegexes);
}

public function provideChunkHeaderCases()
{
yield 'few iris' => [
50,
['/foo', '/bar'],
['((^|\,)/foo($|\,))|((^|\,)/bar($|\,))'],
];

yield 'iris to generate a header with exactly the maximum length' => [
56,
['/foo', '/bar', '/baz'],
['((^|\,)/foo($|\,))|((^|\,)/bar($|\,))|((^|\,)/baz($|\,))'],
];

yield 'iris to generate a header with exactly the maximum length and a smaller one' => [
37,
['/foo', '/bar', '/baz'],
[
'((^|\,)/foo($|\,))|((^|\,)/bar($|\,))',
'(^|\,)/baz($|\,)',
],
];

yield 'with last iri too long to be part of the same header' => [
50,
['/foo', '/bar', '/some-longer-tag'],
[
'((^|\,)/foo($|\,))|((^|\,)/bar($|\,))',
'(^|\,)/some\-longer\-tag($|\,)',
],
];

yield 'iris to have five headers' => [
50,
['/foo/1', '/foo/2', '/foo/3', '/foo/4', '/foo/5', '/foo/6', '/foo/7', '/foo/8', '/foo/9', '/foo/10'],
[
'((^|\,)/foo/1($|\,))|((^|\,)/foo/2($|\,))',
'((^|\,)/foo/3($|\,))|((^|\,)/foo/4($|\,))',
'((^|\,)/foo/5($|\,))|((^|\,)/foo/6($|\,))',
'((^|\,)/foo/7($|\,))|((^|\,)/foo/8($|\,))',
'((^|\,)/foo/9($|\,))|((^|\,)/foo/10($|\,))',
],
];

yield 'with varnish default limit' => [
8000,
array_fill(0, 1000, '/foo'),
[
implode('|', array_fill(0, 421, '((^|\,)/foo($|\,))')),
implode('|', array_fill(0, 421, '((^|\,)/foo($|\,))')),
implode('|', array_fill(0, 158, '((^|\,)/foo($|\,))')),
],
];
}
}

0 comments on commit 544e5c7

Please sign in to comment.