Skip to content

Commit 4661089

Browse files
committed
Merge branch '2.x' into 1.x
* 2.x: "relax" header value assertion / allow more than ascii getDebugType. - use strtolower new `bdk\HttpMessage\Utility\Uri::withParsedValues()` method update workflow - default branch is now "2.x" (not "main") Update readme
2 parents 169cc7e + ef605f1 commit 4661089

File tree

9 files changed

+103
-76
lines changed

9 files changed

+103
-76
lines changed

.github/workflows/phpunit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- name: Unit test
4040
run: composer run test
4141
- name: Publish code coverage
42-
if: ${{ matrix.php-version == '8.3' && github.ref_name == 'main' }}
42+
if: ${{ matrix.php-version == '8.3' && github.ref_name == '2.x' }}
4343
uses: paambaati/[email protected]
4444
continue-on-error: true
4545
env:
@@ -49,6 +49,6 @@ jobs:
4949
coverageCommand: vendor/bin/phpunit --coverage-clover coverage/clover.xml
5050
coverageLocations: coverage/clover.xml:clover
5151
- name: Coverage summary
52-
if: ${{ matrix.php-version == '8.3' && github.ref_name == 'main' }}
52+
if: ${{ matrix.php-version == '8.3' && github.ref_name == '2.x' }}
5353
continue-on-error: true
5454
run: php -f vendor/bdk/devutil/src/coverageChecker.php -- coverage/clover.xml

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ PSR-7 (HttpMessage) & PSR-17 (HttpFactory) Implementations
1212
* `UploadedFile::getClientFullPath()`. PHP 8.1 added a new file upload property (not included in PSR-7)
1313
* `ServerRequestExtended` interface and implementation - Extends standard server request with helpful methods
1414

15+
### Utilities
16+
* ContentType: common mime-type constants
17+
* HttpFoundationBridge: create ServerRequest and Response from HttpFoundation request and response
18+
* ParseStr: PHP's `parse_str()`, but does not convert dots and spaces to '_' by default
19+
* Response:
20+
* `emit(ResponseInterface $response)` - Output response headers and body
21+
* `codePhrase(int|string $code): string` - Get standard code phrase for given HTTP status code
22+
* ServerRequest:
23+
* `fromGlobals(): ServerRequestInterface`
24+
* Stream
25+
* `getContent(StreamInterface): string` - Get stream contents without affecting pointer
26+
* Uri:
27+
* `fromGlobals(): UriInterface`
28+
* `fromParsed(array): UriInterface`
29+
* `isCrossOrigin(UriInterface $uri1, UriInterface $uri2): bool`
30+
* `parseUrl(string|UriInterface): array` - like php's `parse_url` but with bug fixes backported
31+
* `resolve(UriInterface $base, UriInterface $rel): UriInterface` - Converts the relative URI into a new URI that is resolved against the base URI.
32+
1533
### Installation
1634

1735
`composer require bdk/http-message`
@@ -28,23 +46,6 @@ http://bradkent.com/php/httpmessage
2846
|2.x | ^1.1 \| ^2.0 | ^1.0 | >= 7.2 | `self` returns
2947
|1.x | ~1.0.1 | -- | >= 5.4 |   |
3048

31-
32-
33-
### Utilities
34-
* ContentType: common mime-type constants
35-
* HttpFoundationBridge: create ServerRequest and Response from HttpFoundation request and response
36-
* ParseStr: PHP's `parse_str()`, but does not convert dots and spaces to '_' by default
37-
* Response:
38-
* `emit(ResponseInterface $response)` - Output response headers and body
39-
* `codePhrase(int|string $code): string` - Get standard code phrase for given HTTP status code
40-
* ServerRequest:
41-
* `fromGlobals(): ServerRequestInterface`
42-
* Uri:
43-
* `fromGlobals(): UriInterface`
44-
* `isCrossOrigin(UriInterface $uri1, UriInterface $uri2): bool`
45-
* `parseUrl(string|UriInterface): array` - like php's `parse_url` but with bug fixes backported
46-
* `resolve(UriInterface $base, UriInterface $rel): UriInterface` - Converts the relative URI into a new URI that is resolved against the base URI.
47-
4849
## Tests / Quality
4950

5051
![Supported PHP versions](https://img.shields.io/static/v1?label=PHP&message=5.4%20-%208.4&color=blue)

src/HttpMessage/AbstractStream.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected static function getDebugType($value)
4949
{
5050
return \is_object($value)
5151
? \get_class($value)
52-
: \gettype($value);
52+
: \strtolower(\gettype($value));
5353
}
5454

5555
/**

src/HttpMessage/AssertionTrait.php

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ protected static function getDebugType($value)
8080
{
8181
return \is_object($value)
8282
? \get_class($value)
83-
: \gettype($value);
83+
: \strtolower(\gettype($value));
8484
}
8585

8686
/*
@@ -109,7 +109,7 @@ private function assertHeaderName($name)
109109
digit => 0-9
110110
others => !#$%&\'*+-.^_`|~
111111
*/
112-
if (\preg_match('/^[a-zA-Z0-9!#$%&\'*+-.^_`|~]+$/', $name) !== 1) {
112+
if (\preg_match('/^[a-zA-Z0-9!#$%&\'*+-.^_`|~]+$/D', $name) !== 1) {
113113
throw new InvalidArgumentException(\sprintf(
114114
'"%s" is not valid header name, it must be an RFC 7230 compatible string.',
115115
$name
@@ -151,10 +151,21 @@ private function assertHeaderValue($value)
151151
/**
152152
* Validate header value
153153
*
154+
* headers values should be ISO-8859-1 encoded by default
155+
*
156+
* field-value = *( field-content / obs-fold )
157+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
158+
* field-vchar = VCHAR / obs-text
159+
* VCHAR = %x21-7E
160+
* obs-text = %x80-FF
161+
* obs-fold = CRLF 1*( SP / HTAB )
162+
154163
* @param mixed $value Header value to test
155164
*
156165
* @return void
157166
*
167+
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
168+
*
158169
* @throws InvalidArgumentException
159170
*
160171
* @psalm-assert string $value
@@ -165,20 +176,21 @@ private function assertHeaderValueLine($value)
165176
return;
166177
}
167178
$this->assertString($value, 'Header value', true);
168-
/*
169-
https://www.rfc-editor.org/rfc/rfc7230.txt (page.25)
170-
171-
field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
172-
field-vchar = VCHAR / obs-text
173-
obs-text = %x80-FF (character range outside ASCII.)
174-
NOT ALLOWED
175-
SP = space
176-
HTAB = horizontal tab
177-
VCHAR = any visible [USASCII] character. (x21-x7e)
178-
*/
179-
if (\preg_match('/^[ \t\x21-\x7e]+$/', $value) !== 1) {
179+
$value = \trim((string) $value, " \t");
180+
// The regular expression intentionally does not support the obs-fold production, because as
181+
// per RFC 7230#3.2.4:
182+
//
183+
// A sender MUST NOT generate a message that includes
184+
// line folding (i.e., that has any field-value that contains a match to
185+
// the obs-fold rule) unless the message is intended for packaging
186+
// within the message/http media type.
187+
//
188+
// Clients must not send a request with line folding and a server sending folded headers is
189+
// likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting
190+
// folding is not likely to break any legitimate use case.
191+
if (\preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value) !== 1) {
180192
throw new InvalidArgumentException(\sprintf(
181-
'"%s" is not valid header value, it must contains visible ASCII characters only.',
193+
'"%s" is not valid header value.',
182194
$value
183195
));
184196
}

src/HttpMessage/Utility/ParseStr.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ protected static function getDebugType($value)
118118
{
119119
return \is_object($value)
120120
? \get_class($value)
121-
: \gettype($value);
121+
: \strtolower(\gettype($value));
122122
}
123123

124124
/**

src/HttpMessage/Utility/Uri.php

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,8 @@ public static function fromGlobals()
5959
*/
6060
public static function fromParsed(array $parsed)
6161
{
62-
$uriKeys = ['fragment', 'host', 'path', 'port', 'query', 'scheme', 'userInfo'];
63-
$parsed = \array_intersect_key(self::parsedPartsPrep($parsed), \array_flip($uriKeys));
64-
$parsed = \array_filter($parsed, static function ($val) {
65-
return \in_array($val, [null, ''], true) === false;
66-
});
6762
$uri = new BdkUri();
68-
foreach ($parsed as $key => $value) {
69-
$method = 'with' . \ucfirst($key);
70-
/** @var BdkUri */
71-
$uri = \call_user_func_array([$uri, $method], (array) $value);
72-
}
73-
return $uri;
63+
return self::withParsedValues($uri, $parsed);
7464
}
7565

7666
/**
@@ -136,29 +126,57 @@ public static function resolve(UriInterface $base, UriInterface $rel)
136126
return $base;
137127
}
138128
if ($rel->getScheme() !== '') {
139-
// rel specified scheme... return rel (with path cleaned up)
140-
return $rel->withPath(self::pathRemoveDots($rel->getPath()));
129+
// rel specified scheme
130+
// return rel (with path cleaned up)
131+
return $rel
132+
->withPath(self::pathRemoveDots($rel->getPath()));
141133
}
134+
$targetValues = array(
135+
'fragment' => $rel->getFragment(),
136+
);
142137
if ($rel->getAuthority() !== '') {
143-
// rel specified "authority"..
138+
// rel specified "authority"
144139
// return base's scheme, rel's everything else (with path cleaned up)
145-
return $rel
146-
->withScheme($base->getScheme())
147-
->withPath(self::pathRemoveDots($rel->getPath()));
140+
$targetValues['userInfo'] = $rel->getUserInfo();
141+
$targetValues['host'] = $rel->getHost();
142+
$targetValues['port'] = $rel->getPort();
143+
$targetValues['path'] = self::pathRemoveDots($rel->getPath());
144+
$targetValues['query'] = $rel->getQuery();
145+
} elseif ($rel->getPath() !== '') {
146+
// rel specified path
147+
// return base with resolved path (cleaned up), rel's query & fragment
148+
$targetValues['path'] = self::pathRemoveDots(self::resolveTargetPath($base, $rel));
149+
$targetValues['query'] = $rel->getQuery();
150+
} elseif ($rel->getQuery() !== '') {
151+
// rel specified query
152+
$targetValues['query'] = $rel->getQuery();
148153
}
149-
if ($rel->getPath() === '') {
150-
$targetQuery = $rel->getQuery() !== ''
151-
? $rel->getQuery()
152-
: $base->getQuery();
153-
return $base
154-
->withQuery($targetQuery)
155-
->withFragment($rel->getFragment());
154+
return self::withParsedValues($base, $targetValues);
155+
}
156+
157+
/**
158+
* Apply component values to a Uri
159+
*
160+
* @param UriInterface $uri UriInterface instance
161+
* @param array $parsed Component values
162+
*
163+
* @return UriInterface
164+
*
165+
* @since x3.4
166+
*/
167+
public static function withParsedValues(UriInterface $uri, array $values): UriInterface
168+
{
169+
$uriKeys = ['fragment', 'host', 'path', 'port', 'query', 'scheme', 'userInfo'];
170+
$values = \array_intersect_key(self::parsedPartsPrep($values), \array_flip($uriKeys));
171+
foreach ($values as $key => $value) {
172+
$method = 'with' . \ucfirst($key);
173+
// using call_user_func_array... some methods (withUserInfo) accept multiple arguments
174+
$args = $value === null
175+
? array(null)
176+
: (array) $value;
177+
$uri = \call_user_func_array([$uri, $method], $args);
156178
}
157-
$targetPath = self::resolveTargetPath($base, $rel);
158-
return $base
159-
->withPath(self::pathRemoveDots($targetPath))
160-
->withQuery($rel->getQuery())
161-
->withFragment($rel->getFragment());
179+
return $uri;
162180
}
163181

164182
/**
@@ -187,6 +205,7 @@ private static function computePort(UriInterface $uri)
187205
private static function parsedPartsPrep(array $parsed)
188206
{
189207
$map = array(
208+
'passwd' => 'pass',
190209
'password' => 'pass',
191210
'username' => 'user',
192211
);
@@ -358,16 +377,16 @@ private static function uriInterfaceToParts(UriInterface $url)
358377
}
359378

360379
/**
361-
* Get host and port from `$_SERVER` vals
380+
* Get host and port from `$_SERVER` values
362381
*
363-
* @return array{host:string|null,port:int|null} host & port
382+
* @return array{host:string,port:int|null} host & port
364383
*
365384
* @SuppressWarnings(PHPMD.Superglobals)
366385
*/
367386
private static function hostPortFromGlobals()
368387
{
369388
$hostPort = array(
370-
'host' => null,
389+
'host' => '',
371390
'port' => null,
372391
);
373392
if (isset($_SERVER['HTTP_HOST'])) {

tests/HttpMessage/DataProviderTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,15 @@ public function headerValuesInvalid()
261261
'null' => [null],
262262
'object' => [new stdClass()],
263263
'true' => [true],
264+
// 'invisible' => ['This string contains many invisible spaces.'],
264265
];
265266
}
266267

267268
public function headerValuesValid()
268269
{
269270
return [
270271
[1234],
272+
[''], // "opaque data"
271273
['text/plain'],
272274
['PHP 9.1'],
273275
['text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'],

tests/HttpMessage/MessageTest.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,6 @@ public function testWithAddedHeaderArrayValueAndKeys()
120120
Exceptions
121121
*/
122122

123-
public function testExceptionHeaderValueInvalidString()
124-
{
125-
$this->expectException('InvalidArgumentException');
126-
$this->createMessage()
127-
->withHeader('hello-world', 'This string contains many invisible spaces.');
128-
}
129-
130123
public function testWithHeaderRejectsMultipleHostValues()
131124
{
132125
$this->expectException('InvalidArgumentException');

tests/HttpMessage/Utility/UriTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function testParseUrl($url, $expect)
6767
/**
6868
* @dataProvider providerResolve
6969
*/
70-
public function testResolveUri($base, $rel, $expect)
70+
public function testResolve($base, $rel, $expect)
7171
{
7272
$base = new Uri($base);
7373
$rel = new Uri($rel);

0 commit comments

Comments
 (0)