parser = $this->createFormDataParser(); } private function createFormDataParser(?ParserConfig $config = null): FormDataParser { // Create parser cache with proper serialization $baseCache = new GeneralCache(new InMemoryCache(), new \App\Framework\Serializer\Php\PhpSerializer()); $compressionCache = new CompressionCacheDecorator( $baseCache, new NullCompression(), new PhpSerializer() ); $cache = new ParserCache($compressionCache); $config = $config ?? new ParserConfig(); // FormDataParser needs QueryStringParser as second parameter $queryParser = new QueryStringParser($config, $cache); return new FormDataParser($config, $queryParser); } public function testParseEmptyBody(): void { $result = $this->parser->parse('application/x-www-form-urlencoded', ''); $this->assertSame([], $result); } public function testParseUrlEncodedSimple(): void { $body = 'name=John&email=john@example.com&age=30'; $result = $this->parser->parse('application/x-www-form-urlencoded', $body); $this->assertSame([ 'name' => 'John', 'email' => 'john@example.com', 'age' => '30', ], $result); } public function testParseUrlEncodedWithSpaces(): void { $body = 'message=Hello+World&city=New+York'; $result = $this->parser->parse('application/x-www-form-urlencoded', $body); $this->assertSame([ 'message' => 'Hello World', 'city' => 'New York', ], $result); } public function testParseUrlEncodedWithSpecialChars(): void { $body = 'email=test%40example.com&message=Hello%21+How%3F'; $result = $this->parser->parse('application/x-www-form-urlencoded', $body); $this->assertSame([ 'email' => 'test@example.com', 'message' => 'Hello! How?', ], $result); } public function testParseUrlEncodedArrays(): void { $body = 'tags[]=php&tags[]=web&user[name]=John&user[age]=30'; $result = $this->parser->parse('application/x-www-form-urlencoded', $body); $this->assertSame([ 'tags' => ['php', 'web'], 'user' => [ 'name' => 'John', 'age' => '30', ], ], $result); } public function testParseUrlEncodedCaseInsensitiveContentType(): void { $body = 'name=John'; $result = $this->parser->parse('APPLICATION/X-WWW-FORM-URLENCODED', $body); $this->assertSame(['name' => 'John'], $result); } public function testParseUrlEncodedWithCharset(): void { $body = 'name=John&message=Hello'; $result = $this->parser->parse('application/x-www-form-urlencoded; charset=utf-8', $body); $this->assertSame([ 'name' => 'John', 'message' => 'Hello', ], $result); } public function testParseMultipartSimpleFields(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"username\"\r\n" . "\r\n" . "johndoe\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"email\"\r\n" . "\r\n" . "john@example.com\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame([ 'username' => 'johndoe', 'email' => 'john@example.com', ], $result); } public function testParseMultipartWithEmptyField(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"username\"\r\n" . "\r\n" . "johndoe\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"optional\"\r\n" . "\r\n" . "\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame([ 'username' => 'johndoe', 'optional' => '', ], $result); } public function testParseMultipartArrayFields(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"tags[]\"\r\n" . "\r\n" . "php\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"tags[]\"\r\n" . "\r\n" . "web\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"user[name]\"\r\n" . "\r\n" . "John\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame([ 'tags' => ['php', 'web'], 'user' => ['name' => 'John'], ], $result); } public function testParseMultipartSkipsFileFields(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"username\"\r\n" . "\r\n" . "johndoe\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n" . "Content-Type: text/plain\r\n" . "\r\n" . "File content here\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); // Should only include regular form fields, not files $this->assertSame([ 'username' => 'johndoe', ], $result); } public function testParseMultipartWithContentType(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"data\"\r\n" . "Content-Type: application/json\r\n" . "\r\n" . "{\"key\":\"value\"}\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame([ 'data' => '{"key":"value"}', ], $result); } public function testParseMultipartMissingBoundaryThrowsException(): void { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Missing boundary in multipart/form-data'); $this->parser->parse('multipart/form-data', 'some body'); } public function testParseUnsupportedContentType(): void { $result = $this->parser->parse('application/json', '{"key":"value"}'); $this->assertSame([], $result); } public function testParseMultipartWithQuotedBoundary(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"test\"\r\n" . "\r\n" . "value\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary="----FormBoundary123"', $body); $this->assertSame(['test' => 'value'], $result); } public function testParseMultipartIgnoresMalformedParts(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"valid\"\r\n" . "\r\n" . "validvalue\r\n" . "------FormBoundary123\r\n" . "Malformed part without proper headers\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"another\"\r\n" . "\r\n" . "anothervalue\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame([ 'valid' => 'validvalue', 'another' => 'anothervalue', ], $result); } public function testParseMultipartWithSpacesInDisposition(): void { $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data ; name = \"test\" \r\n" . "\r\n" . "value\r\n" . "------FormBoundary123--\r\n"; $result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame(['test' => 'value'], $result); } // Security Tests public function testFormDataSizeExceeded(): void { $config = new ParserConfig(maxFormDataSize: new Byte(50)); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Form data size exceeded'); $longBody = str_repeat('a=value&', 20); // Creates a long form body $parser->parse('application/x-www-form-urlencoded', $longBody); } public function testMultipartBoundaryTooLong(): void { $config = new ParserConfig(maxBoundaryLength: 10); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Multipart boundary too long'); $longBoundary = str_repeat('a', 15); $body = "--{$longBoundary}\r\nContent-Disposition: form-data; name=\"test\"\r\n\r\nvalue\r\n--{$longBoundary}--\r\n"; $parser->parse("multipart/form-data; boundary={$longBoundary}", $body); } public function testMultipartPartsCountExceeded(): void { $config = new ParserConfig(maxMultipartParts: 2); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Multipart parts exceeded'); $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"field1\"\r\n\r\nvalue1\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"field2\"\r\n\r\nvalue2\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"field3\"\r\n\r\nvalue3\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testFieldCountExceeded(): void { $config = new ParserConfig(maxFieldCount: 2); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Field count exceeded'); $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"field1\"\r\n\r\nvalue1\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"field2\"\r\n\r\nvalue2\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"field3\"\r\n\r\nvalue3\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testFieldNameTooLong(): void { $config = new ParserConfig(maxFieldNameLength: 10); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Field name too long'); $boundary = '----FormBoundary123'; $longFieldName = str_repeat('a', 15); $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"{$longFieldName}\"\r\n\r\nvalue\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testFieldValueTooLong(): void { $config = new ParserConfig(maxFieldValueLength: 10); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Field value too long'); $boundary = '----FormBoundary123'; $longValue = str_repeat('a', 15); $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"test\"\r\n\r\n{$longValue}\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testMaliciousScriptInjection(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Suspicious content detected'); $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"evil\"\r\n\r\n\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testSqlInjectionDetected(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('SQL injection attempt detected'); $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"query\"\r\n\r\nUNION SELECT * FROM users\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testPathTraversalDetected(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Path traversal attempt detected'); $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"path\"\r\n\r\n../../../etc/passwd\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testControlCharactersDetected(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Control characters detected'); $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"data\"\r\n\r\nvalue\x00nullbyte\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } public function testExcessiveRepetitionDetected(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Excessive character repetition detected'); $boundary = '----FormBoundary123'; $repetitiveValue = str_repeat('A', 150); // More than 100 repetitions $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"dos\"\r\n\r\n{$repetitiveValue}\r\n" . "------FormBoundary123--\r\n"; $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); } // URL-encoded Security Tests public function testUrlEncodedFormDataSizeExceeded(): void { $config = new ParserConfig(maxFormDataSize: new Byte(20)); $parser = $this->createFormDataParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Form data size exceeded'); $longBody = 'field=' . str_repeat('a', 50); $parser->parse('application/x-www-form-urlencoded', $longBody); } // Security Configuration Tests public function testSecurityDisabled(): void { $config = new ParserConfig( scanForMaliciousContent: false, maxFieldCount: 1000, maxFieldNameLength: 1000, maxFieldValueLength: 1000, maxFormDataSize: new Byte(10 * 1024 * 1024) ); $parser = $this->createFormDataParser($config); // Should not throw exception when security is disabled $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"evil\"\r\n\r\n\r\n" . "------FormBoundary123--\r\n"; $result = $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame(['evil' => ''], $result); } public function testWithinSecurityLimits(): void { $config = new ParserConfig( maxFieldCount: 5, maxFieldNameLength: 20, maxFieldValueLength: 50, maxFormDataSize: new Byte(1024), scanForMaliciousContent: true ); $parser = $this->createFormDataParser($config); // Should work fine within limits $boundary = '----FormBoundary123'; $body = "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"username\"\r\n\r\njohndoe\r\n" . "------FormBoundary123\r\n" . "Content-Disposition: form-data; name=\"email\"\r\n\r\njohn@example.com\r\n" . "------FormBoundary123--\r\n"; $result = $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body); $this->assertSame([ 'username' => 'johndoe', 'email' => 'john@example.com', ], $result); } }