From e80eb635a429480759c455f13d609205bad09ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=BE=E9=9C=AD?= <15516319695@163.com> Date: Wed, 5 Nov 2025 22:41:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor(http-message):=20=E7=A7=BB=E9=99=A4=20?= =?UTF-8?q?laminas/laminas-mime=20=E4=BE=9D=E8=B5=96=E5=B9=B6=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E4=B8=BA=E8=87=AA=E5=AE=9A=E4=B9=89=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8-=20=E5=88=A0=E9=99=A4=20composer.json=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20laminas/laminas-mime=20=E4=BE=9D=E8=B5=96=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Hyperf\HttpMessage\Util\HeaderFieldParser?= =?UTF-8?q?=20=E5=B7=A5=E5=85=B7=E7=B1=BB-=20=E5=AE=9E=E7=8E=B0=20splitHea?= =?UTF-8?q?derField=20=E6=96=B9=E6=B3=95=E6=9B=BF=E4=BB=A3=E5=8E=9F?= =?UTF-8?q?=E6=9C=89=E7=9A=84=20Decode::splitHeaderField=20-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20splitContentType=20=E4=BE=BF=E6=8D=B7=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E7=94=A8=E4=BA=8E=E8=A7=A3=E6=9E=90=20Content-Type=20?= =?UTF-8?q?=E5=A4=B4=E9=83=A8=20-=20=E5=9C=A8=20MessageTrait=20=E4=B8=AD?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B0=E7=9A=84=20HeaderFieldParser=20?= =?UTF-8?q?=E7=B1=BB=20-=20=E6=B7=BB=E5=8A=A0=E5=AE=8C=E6=95=B4=E7=9A=84?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E5=90=84?= =?UTF-8?q?=E7=A7=8D=E8=A7=A3=E6=9E=90=E5=9C=BA=E6=99=AF=20-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=A7=A3=E6=9E=90=E5=B8=A6=E5=BC=95=E5=8F=B7=E7=9A=84?= =?UTF-8?q?=E5=80=BC=E5=92=8C=E5=A4=A7=E5=B0=8F=E5=86=99=E4=B8=8D=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E7=9A=84=E5=8F=82=E6=95=B0=E5=90=8D=20-=20=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E5=AF=B9=20multipart=20=E8=BE=B9=E7=95=8C=E5=92=8C=20?= =?UTF-8?q?Content-Disposition=20=E5=A4=B4=E9=83=A8=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 - src/Base/MessageTrait.php | 4 +- src/Util/HeaderFieldParser.php | 109 +++++++++++ tests/Util/HeaderFieldParserTest.php | 268 +++++++++++++++++++++++++++ 4 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 src/Util/HeaderFieldParser.php create mode 100644 tests/Util/HeaderFieldParserTest.php 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', + ], + ]; + } +}