diff --git a/composer.json b/composer.json index fad2d18..d652fbb 100755 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ "hyperf/coroutine": "~3.1.0", "hyperf/engine": "^2.11", "hyperf/support": "~3.1.0", - "laminas/laminas-mime": "^2.7", "psr/http-message": "^1.0 || ^2.0", "swow/psr7-plus": "^1.0" }, diff --git a/src/Base/MessageTrait.php b/src/Base/MessageTrait.php index cd0a881..1cff65c 100755 --- a/src/Base/MessageTrait.php +++ b/src/Base/MessageTrait.php @@ -13,8 +13,8 @@ namespace Hyperf\HttpMessage\Base; use Hyperf\HttpMessage\Stream\SwooleStream; +use Hyperf\HttpMessage\Util\HeaderFieldParser; use InvalidArgumentException; -use Laminas\Mime\Decode; use Psr\Http\Message\StreamInterface; use RuntimeException; use Throwable; @@ -299,7 +299,7 @@ public function withBody(StreamInterface $body): static */ public function getHeaderField(string $name, string $wantedPart = '0', string $firstName = '0') { - return Decode::splitHeaderField($this->getHeaderLine($name), $wantedPart, $firstName); + return HeaderFieldParser::splitHeaderField($this->getHeaderLine($name), $wantedPart, $firstName); } public function getContentType(): string diff --git a/src/Util/HeaderFieldParser.php b/src/Util/HeaderFieldParser.php new file mode 100644 index 0000000..484e8bb --- /dev/null +++ b/src/Util/HeaderFieldParser.php @@ -0,0 +1,109 @@ + $name) { + if (strcasecmp($name, $wantedPart) !== 0) { + continue; + } + // Remove quotes if present + if ($matches[2][$key][0] !== '"') { + return $matches[2][$key]; + } + return substr($matches[2][$key], 1, -1); + } + return null; + } + + // Return all parts as associative array + $split = []; + foreach ($matches[1] as $key => $name) { + $name = strtolower($name); + // Remove quotes if present + if ($matches[2][$key][0] === '"') { + $split[$name] = substr($matches[2][$key], 1, -1); + } else { + $split[$name] = $matches[2][$key]; + } + } + + return $split; + } + + /** + * Split a Content-Type header into its different parts. + * + * Convenience method for parsing Content-Type headers. + * Returns type and parameters (charset, boundary, etc.). + * + * @param string $type the content-type header value + * @param string|null $wantedPart the wanted part, or null to return all parts + * @return string|array|null wanted part or all parts as array('type' => content-type, partname => value) + */ + public static function splitContentType(string $type, ?string $wantedPart = null): string|array|null + { + return self::splitHeaderField($type, $wantedPart, 'type'); + } +} diff --git a/tests/Util/HeaderFieldParserTest.php b/tests/Util/HeaderFieldParserTest.php new file mode 100644 index 0000000..a2859d6 --- /dev/null +++ b/tests/Util/HeaderFieldParserTest.php @@ -0,0 +1,268 @@ +assertIsArray($result); + $this->assertEquals($expected, $result); + } + + /** + * Test splitting header field to get a specific part. + */ + #[DataProvider('provideHeaderFieldsForSpecificPart')] + public function testSplitHeaderFieldSpecificPart(string $field, string $wantedPart, ?string $expected, string $firstName = '0'): void + { + $result = HeaderFieldParser::splitHeaderField($field, $wantedPart, $firstName); + $this->assertEquals($expected, $result); + } + + /** + * Test splitting Content-Type header. + */ + #[DataProvider('provideContentTypes')] + public function testSplitContentType(string $contentType, ?string $wantedPart, string|array|null $expected): void + { + $result = HeaderFieldParser::splitContentType($contentType, $wantedPart); + $this->assertEquals($expected, $result); + } + + /** + * Test that invalid header field throws exception. + */ + public function testInvalidHeaderFieldThrowsException(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('not a valid header field'); + HeaderFieldParser::splitHeaderField(''); + } + + /** + * Test getting the first part only. + */ + #[DataProvider('provideHeaderFieldsForFirstPart')] + public function testSplitHeaderFieldFirstPart(string $field, string $expected): void + { + $result = HeaderFieldParser::splitHeaderField($field, '0', '0'); + $this->assertEquals($expected, $result); + } + + /** + * Test with quoted values containing special characters. + */ + public function testQuotedValuesWithSpecialCharacters(): void + { + $field = 'text/html; charset="utf-8"; name="file; name.txt"'; + $result = HeaderFieldParser::splitHeaderField($field, null, 'type'); + + $this->assertEquals([ + 'type' => 'text/html', + 'charset' => 'utf-8', + 'name' => 'file; name.txt', + ], $result); + } + + /** + * Test case insensitive parameter names. + */ + public function testCaseInsensitiveParameterNames(): void + { + $field = 'text/html; Charset=utf-8; BOUNDARY=something'; + $result = HeaderFieldParser::splitHeaderField($field, null, 'type'); + + $this->assertEquals([ + 'type' => 'text/html', + 'charset' => 'utf-8', + 'boundary' => 'something', + ], $result); + } + + /** + * Test retrieving non-existent part returns null. + */ + public function testNonExistentPartReturnsNull(): void + { + $field = 'text/html; charset=utf-8'; + $result = HeaderFieldParser::splitHeaderField($field, 'boundary', 'type'); + + $this->assertNull($result); + } + + /** + * Test Content-Disposition header. + */ + public function testContentDispositionHeader(): void + { + $field = 'attachment; filename="document.pdf"; size=12345'; + $result = HeaderFieldParser::splitHeaderField($field, null, 'disposition'); + + $this->assertEquals([ + 'disposition' => 'attachment', + 'filename' => 'document.pdf', + 'size' => '12345', + ], $result); + } + + /** + * Test multipart boundary extraction. + */ + public function testMultipartBoundaryExtraction(): void + { + $field = 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW'; + $boundary = HeaderFieldParser::splitContentType($field, 'boundary'); + + $this->assertEquals('----WebKitFormBoundary7MA4YWxkTrZu0gW', $boundary); + } + + /** + * Data provider for all parts test. + */ + public static function provideHeaderFieldsForAllParts(): array + { + return [ + 'simple content-type' => [ + 'text/html', + ['0' => 'text/html'], + ], + 'content-type with charset' => [ + 'text/html; charset=utf-8', + ['0' => 'text/html', 'charset' => 'utf-8'], + ], + 'content-type with multiple params' => [ + 'text/html; charset=utf-8; boundary=something', + ['0' => 'text/html', 'charset' => 'utf-8', 'boundary' => 'something'], + ], + 'quoted values' => [ + 'text/html; charset="utf-8"; name="test.txt"', + ['0' => 'text/html', 'charset' => 'utf-8', 'name' => 'test.txt'], + ], + 'multipart with boundary' => [ + 'multipart/form-data; boundary=----WebKitFormBoundary', + ['0' => 'multipart/form-data', 'boundary' => '----WebKitFormBoundary'], + ], + 'with spaces around equals' => [ + 'text/html; charset = utf-8; name = "file.txt"', + ['0' => 'text/html', 'charset' => 'utf-8', 'name' => 'file.txt'], + ], + ]; + } + + /** + * Data provider for specific part test. + */ + public static function provideHeaderFieldsForSpecificPart(): array + { + return [ + 'get charset' => [ + 'text/html; charset=utf-8', + 'charset', + 'utf-8', + '0', + ], + 'get boundary' => [ + 'multipart/form-data; boundary=something', + 'boundary', + 'something', + '0', + ], + 'get quoted value' => [ + 'text/html; name="test.txt"', + 'name', + 'test.txt', + '0', + ], + 'non-existent part' => [ + 'text/html; charset=utf-8', + 'boundary', + null, + '0', + ], + 'case insensitive lookup' => [ + 'text/html; Charset=utf-8', + 'charset', + 'utf-8', + '0', + ], + ]; + } + + /** + * Data provider for Content-Type test. + */ + public static function provideContentTypes(): array + { + return [ + 'get type only' => [ + 'text/html; charset=utf-8', + 'type', + 'text/html', + ], + 'get charset' => [ + 'text/html; charset=utf-8', + 'charset', + 'utf-8', + ], + 'get all parts' => [ + 'text/html; charset=utf-8; boundary=test', + null, + ['type' => 'text/html', 'charset' => 'utf-8', 'boundary' => 'test'], + ], + 'simple type' => [ + 'application/json', + 'type', + 'application/json', + ], + ]; + } + + /** + * Data provider for first part test. + */ + public static function provideHeaderFieldsForFirstPart(): array + { + return [ + 'simple value' => [ + 'text/html; charset=utf-8', + 'text/html', + ], + 'quoted value' => [ + '"text/html"; charset=utf-8', + 'text/html', + ], + 'no parameters' => [ + 'application/json', + 'application/json', + ], + ]; + } +}