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);
}
}