Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/Web/DevServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ public static function handleConnection($conn, string $docroot, callable $handle

self::clearHttpServerKeys();
$cgiEnv = array_merge($cgiEnv, Superglobals::applyHttpHeaders($headers));
$contentLength = self::contentLengthForRequest($headers, $body);
if (null !== $contentLength) {
$cgiEnv['CONTENT_LENGTH'] = $contentLength;
$_SERVER['CONTENT_LENGTH'] = $contentLength;
putenv('CONTENT_LENGTH='.$contentLength);
}

putenv('REQUEST_METHOD='.$method);
putenv('QUERY_STRING='.$query);
Expand Down Expand Up @@ -205,6 +211,21 @@ public static function readRequest($conn): ?array
return [$method, $path, $query, $headers, $body];
}

/**
* CGI CONTENT_LENGTH for incoming requests when Content-Length was sent.
*
* Uses the bytes actually read (not the header alone). Absent for chunked
* requests without Content-Length (issue #287).
*/
public static function contentLengthForRequest(array $headers, string $body): ?string
{
if (!isset($headers['content-length'])) {
return null;
}

return (string) strlen($body);
}

/**
* Map an HTTP header name to a CGI $_SERVER key (e.g. host → HTTP_HOST).
*/
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/aot/cases/web_content_length.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
AOT: CONTENT_LENGTH from CGI env (issue #314)
--ENV--
REQUEST_METHOD=POST
CONTENT_LENGTH=12
REQUEST_BODY=abcdefghijkl
--FILE--
<?php
echo $_SERVER['CONTENT_LENGTH'];
--EXPECT--
12
--EXPECT_EXIT--
0
76 changes: 76 additions & 0 deletions test/real/ServeAotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ public function testServeAotPopulatesDocumentRoot(): void
@rmdir($binaryDir);
}

public function testServeAotPopulatesContentLengthOnPost(): void
{
$body = 'abcdefghijkl';
$docroot = $this->makeDocroot([
'length.php' => <<<'PHP'
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo $_SERVER['CONTENT_LENGTH'];
PHP,
]);
$binaryDir = sys_get_temp_dir().'/phpc_serve_aot_cl_'.bin2hex(random_bytes(4));
$this->assertTrue(mkdir($binaryDir));
$binary = $binaryDir.'/app';
$this->compileExample($docroot.'/length.php', $binary);
$response = $this->httpPostAot($docroot, $binary, '/length.php', $body);
$this->assertStringContainsString('HTTP/1.1 200', $response);
$this->assertStringContainsString('12', $response);
@unlink($binary);
@rmdir($binaryDir);
}

public function testServeAot001SimpleWeb(): void
{
$docroot = $this->repoRoot.'/examples/001-SimpleWeb';
Expand Down Expand Up @@ -174,6 +196,60 @@ private function httpGetAot(string $docroot, string $binary, string $path): stri
return $response !== false ? $response : '';
}

private function httpPostAot(string $docroot, string $binary, string $path, string $body): string
{
$port = $this->findFreePort();
$addr = "127.0.0.1:{$port}";
$env = array_merge($this->baseEnv(), $this->llvmEnv());
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$cmd = array_merge(
$this->phpCmd,
[$this->repoRoot.'/bin/serve-aot.php', $addr, $docroot, '--binary', $binary]
);
$proc = proc_open($cmd, $descriptorSpec, $pipes, $this->repoRoot, $env);
$this->assertIsResource($proc);
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);

$deadline = microtime(true) + 15.0;
$ready = false;
while (microtime(true) < $deadline) {
$conn = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.2);
if (false !== $conn) {
$ready = true;
fclose($conn);
break;
}
usleep(50_000);
}
$this->assertTrue($ready, 'serve-aot did not become ready');

$conn = fsockopen('127.0.0.1', $port);
$this->assertIsResource($conn);
$len = strlen($body);
fwrite(
$conn,
"POST {$path} HTTP/1.1\r\n"
."Host: 127.0.0.1\r\n"
."Content-Type: application/x-www-form-urlencoded\r\n"
."Content-Length: {$len}\r\n"
."Connection: close\r\n\r\n"
.$body
);
$response = stream_get_contents($conn);
fclose($conn);

proc_terminate($proc);
proc_close($proc);

return $response !== false ? $response : '';
}

/**
* @return array<string, string>
*/
Expand Down
66 changes: 66 additions & 0 deletions test/real/ServeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ public function testPopulatesCookieFromRequestHeader(): void
$this->assertStringContainsString('dark', $response);
}

public function testPopulatesContentLengthOnPost(): void
{
$body = 'abcdefghijkl';
$docroot = $this->makeDocroot([
'length.php' => <<<'PHP'
<?php
header('Content-Type: text/plain; charset=UTF-8');
echo $_SERVER['CONTENT_LENGTH'];
PHP,
]);
$response = $this->httpPost($docroot, '/length.php', $body);
$this->assertStringContainsString('HTTP/1.1 200', $response);
$this->assertStringContainsString('12', $response);
}

/**
* @param array<string, string> $extraEnv
* @param list<string> $extraRequestHeaders
Expand Down Expand Up @@ -197,6 +212,57 @@ private function httpGet(string $docroot, string $path, array $extraEnv = [], ar
return $response !== false ? $response : '';
}

private function httpPost(string $docroot, string $path, string $body, array $extraEnv = []): string
{
$port = $this->findFreePort();
$addr = "127.0.0.1:{$port}";
$env = array_merge($this->baseEnv(), $extraEnv);
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$cmd = array_merge($this->phpCmd, [$this->repoRoot.'/bin/serve.php', $addr, $docroot]);
$proc = proc_open($cmd, $descriptorSpec, $pipes, $this->repoRoot, $env);
$this->assertIsResource($proc);
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);

$deadline = microtime(true) + 5.0;
$ready = false;
while (microtime(true) < $deadline) {
$conn = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.2);
if (false !== $conn) {
$ready = true;
fclose($conn);
break;
}
usleep(50_000);
}
$this->assertTrue($ready, 'serve did not become ready');

$conn = fsockopen('127.0.0.1', $port);
$this->assertIsResource($conn);
$len = strlen($body);
fwrite(
$conn,
"POST {$path} HTTP/1.1\r\n"
."Host: 127.0.0.1\r\n"
."Content-Type: application/x-www-form-urlencoded\r\n"
."Content-Length: {$len}\r\n"
."Connection: close\r\n\r\n"
.$body
);
$response = stream_get_contents($conn);
fclose($conn);

proc_terminate($proc);
proc_close($proc);

return $response !== false ? $response : '';
}

/**
* @param array<string, string> $files relative path => contents
*/
Expand Down
13 changes: 13 additions & 0 deletions test/real/cases/web_content_length.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Web: CONTENT_LENGTH from CGI env (issue #314)
--ENV--
REQUEST_METHOD=POST
CONTENT_LENGTH=12
REQUEST_BODY=abcdefghijkl
--FILE--
<?php
echo $_SERVER['CONTENT_LENGTH'];
--EXPECT--
12
--EXPECT_EXIT--
0
7 changes: 7 additions & 0 deletions test/unit/Web/DevServerHeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,11 @@ public function testRejectsHeaderValueWithNewlines(): void
]);
$this->assertSame([], $vars);
}

public function testContentLengthForRequestUsesBodySize(): void
{
$body = 'abcdefghijkl';
$this->assertSame('12', DevServer::contentLengthForRequest(['content-length' => '99'], $body));
$this->assertNull(DevServer::contentLengthForRequest([], $body));
}
}
56 changes: 56 additions & 0 deletions test/unit/Web/SuperglobalsContentLengthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPCompiler\Web\DevServer;
use PHPCompiler\Web\Superglobals;
use PHPUnit\Framework\TestCase;

/**
* Issue #314: $_SERVER['CONTENT_LENGTH'] from HTTP Content-Length header.
*/
final class SuperglobalsContentLengthTest extends TestCase
{
protected function tearDown(): void
{
putenv('CONTENT_LENGTH');
foreach (array_keys($_SERVER) as $key) {
if ('CONTENT_LENGTH' === $key) {
unset($_SERVER[$key]);
}
}
}

public function testContentLengthForRequestUsesReadBodySize(): void
{
$body = 'abcdefghijkl';
$this->assertSame('12', DevServer::contentLengthForRequest(['content-length' => '12'], $body));
}

public function testContentLengthForRequestAbsentWithoutHeader(): void
{
$this->assertNull(DevServer::contentLengthForRequest([], 'body'));
}

public function testApplyHttpHeadersMapsContentLength(): void
{
$cgi = Superglobals::applyHttpHeaders(['content-length' => '12']);
$this->assertSame(['CONTENT_LENGTH' => '12'], $cgi);
$this->assertSame('12', $_SERVER['CONTENT_LENGTH'] ?? '');
}

public function testPopulateFromEnvironmentReadsContentLength(): void
{
putenv('CONTENT_LENGTH=12');
$_SERVER['CONTENT_LENGTH'] = '12';
$runtime = new Runtime();
Superglobals::populateFromEnvironment($runtime->vmContext, '', 'abcdefghijkl');

$server = $runtime->vmContext->getSuperglobal('_SERVER')->toArray();
$var = $server->find('CONTENT_LENGTH');
$this->assertNotNull($var);
$this->assertSame('12', $var->resolveIndirect()->toString());
}
}
Loading