Skip to content

Commit f016240

Browse files
committed
Added a TagHeaderFormatter that limits the header values from another formatter to a given size
1 parent 508d232 commit f016240

File tree

4 files changed

+194
-2
lines changed

4 files changed

+194
-2
lines changed

src/Exception/InvalidTagException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace FOS\HttpCache\Exception;
1313

1414
/**
15-
* Thrown during tagging with an empty value.
15+
* Thrown during tagging with an invalid value.
1616
*/
1717
class InvalidTagException extends \InvalidArgumentException implements HttpCacheException
1818
{
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCache\TagHeaderFormatter;
13+
14+
use FOS\HttpCache\Exception\InvalidTagException;
15+
16+
/**
17+
* A header formatter that splits the value(s) from a given
18+
* other header formatter (e.g. the CommaSeparatedTagHeaderFormatter)
19+
* into multiple headers making sure none of the header values
20+
* exceeds the configured limit
21+
*
22+
* @author Yanick Witschi <[email protected]>
23+
*/
24+
class MaxHeaderValueLengthFormatter implements TagHeaderFormatter
25+
{
26+
/**
27+
* @var TagHeaderFormatter
28+
*/
29+
private $inner;
30+
31+
/**
32+
* @var int
33+
*/
34+
private $maxHeaderValueLength;
35+
36+
/**
37+
* The default value of the maximum header length is 4096 because most
38+
* servers limit header values to 4kb.
39+
* HTTP messages cannot carry characters outside the ISO-8859-1 standard so they all
40+
* use up just one byte.
41+
*
42+
* @param TagHeaderFormatter $inner
43+
* @param int $maxHeaderValueLength
44+
*/
45+
public function __construct(TagHeaderFormatter $inner, $maxHeaderValueLength = 4096)
46+
{
47+
$this->inner = $inner;
48+
$this->maxHeaderValueLength = $maxHeaderValueLength;
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function getTagsHeaderName()
55+
{
56+
$this->inner->getTagsHeaderName();
57+
}
58+
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
public function getTagsHeaderValue(array $tags)
64+
{
65+
$values = (array) $this->inner->getTagsHeaderValue($tags);
66+
$newValues = [[]];
67+
68+
foreach ($values as $value) {
69+
if ($this->isHeaderTooLong($value)) {
70+
list ($firstTags, $secondTags) = $this->splitTagsInHalves($tags);
71+
72+
$newValues[] = (array) $this->getTagsHeaderValue($firstTags);
73+
$newValues[] = (array) $this->getTagsHeaderValue($secondTags);
74+
} else {
75+
$newValues[] = [$value];
76+
}
77+
}
78+
79+
$newValues = array_merge(...$newValues);
80+
81+
if (1 === count($newValues)) {
82+
return $newValues[0];
83+
}
84+
85+
return $newValues;
86+
}
87+
88+
/**
89+
* @param string $value
90+
*
91+
* @return bool
92+
*/
93+
private function isHeaderTooLong($value)
94+
{
95+
return mb_strlen($value) > $this->maxHeaderValueLength;
96+
}
97+
98+
/**
99+
* Split an array of tags in two more or less equal sized arrays.
100+
*
101+
* @param array $tags
102+
*
103+
* @return array
104+
* @throws InvalidTagException
105+
*/
106+
private function splitTagsInHalves(array $tags) {
107+
108+
if (1 === count($tags)) {
109+
throw new InvalidTagException(sprintf(
110+
'You configured a maximum header length of %d but the tag "%s" is too long.',
111+
$this->maxHeaderValueLength,
112+
$tags[0]
113+
));
114+
}
115+
116+
$size = ceil(count($tags) / 2);
117+
return array_chunk($tags, $size);
118+
}
119+
}

src/TagHeaderFormatter/TagHeaderFormatter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function getTagsHeaderName();
3939
*
4040
* @param array $tags
4141
*
42-
* @return string
42+
* @return string|string[]
4343
*/
4444
public function getTagsHeaderValue(array $tags);
4545
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCache\Tests\Unit\TagHeaderFormatter;
13+
14+
15+
use FOS\HttpCache\Exception\InvalidTagException;
16+
use FOS\HttpCache\TagHeaderFormatter\CommaSeparatedTagHeaderFormatter;
17+
use FOS\HttpCache\TagHeaderFormatter\MaxHeaderValueLengthFormatter;
18+
use PHPUnit\Framework\TestCase;
19+
20+
class MaxHeaderValueLengthFormatterTest extends TestCase
21+
{
22+
public function testNotTooLong()
23+
{
24+
$formatter = $this->getFormatter(50);
25+
$tags = ['foo', 'bar', 'baz'];
26+
27+
$this->assertSame('foo,bar,baz', $formatter->getTagsHeaderValue($tags));
28+
}
29+
30+
/**
31+
* @dataProvider tooLongProvider
32+
* @param int $maxLength
33+
* @param array $tags
34+
* @param mixed $expectedHeaderValue
35+
*/
36+
public function testTooLong($maxLength, $tags, $expectedHeaderValue)
37+
{
38+
$formatter = $this->getFormatter($maxLength);
39+
$this->assertSame($expectedHeaderValue, $formatter->getTagsHeaderValue($tags));
40+
}
41+
42+
public function testOneTagExceedsMaximum()
43+
{
44+
$this->expectException(InvalidTagException::class);
45+
$this->expectExceptionMessage('You configured a maximum header length of 3 but the tag "way-too-long-tag" is too long.');
46+
47+
$formatter = $this->getFormatter(3);
48+
$formatter->getTagsHeaderValue(['way-too-long-tag']);
49+
}
50+
51+
/**
52+
* @param int $maxLength
53+
*
54+
* @return MaxHeaderValueLengthFormatter
55+
*/
56+
private function getFormatter($maxLength)
57+
{
58+
return new MaxHeaderValueLengthFormatter(
59+
new CommaSeparatedTagHeaderFormatter(),
60+
$maxLength
61+
);
62+
}
63+
64+
public function tooLongProvider()
65+
{
66+
return [
67+
[3, ['foo', 'bar', 'baz'], ['foo', 'bar', 'baz']],
68+
[6, ['foo', 'bar', 'baz'], ['foo', 'bar', 'baz']], // with the , that equals 7
69+
[7, ['foo', 'bar', 'baz'], ['foo,bar', 'baz']],
70+
[50, ['foo', 'bar', 'baz'], 'foo,bar,baz'], // long enough, must not be an array
71+
];
72+
}
73+
}

0 commit comments

Comments
 (0)