diff --git a/bin/compile.php b/bin/compile.php index 5b3c7ccf..bcdee32a 100755 --- a/bin/compile.php +++ b/bin/compile.php @@ -17,10 +17,18 @@ function run(string $filename, string $code, array $options): void $runtime = new Runtime(Runtime::MODE_AOT); $queryString = $options['-q'] ?? null; $postBody = $options['-p'] ?? null; + $scriptFilename = null; + if ('-' !== $filename && 'Command line code' !== $filename) { + $resolved = realpath($filename); + if (false !== $resolved) { + $scriptFilename = $resolved; + } + } Superglobals::populateFromEnvironment( $runtime->vmContext, is_string($queryString) ? $queryString : null, - is_string($postBody) ? $postBody : null + is_string($postBody) ? $postBody : null, + $scriptFilename ); $block = $runtime->parseAndCompile($code, $filename); if (! isset($options['-l'])) { diff --git a/bin/vm.php b/bin/vm.php index 1cabc2a6..61ea763b 100755 --- a/bin/vm.php +++ b/bin/vm.php @@ -17,10 +17,18 @@ function run(string $filename, string $code, array $options): void $runtime = new Runtime(); $queryString = $options['-q'] ?? null; $postBody = $options['-p'] ?? null; + $scriptFilename = null; + if ('-' !== $filename && 'Command line code' !== $filename) { + $resolved = realpath($filename); + if (false !== $resolved) { + $scriptFilename = $resolved; + } + } Superglobals::populateFromEnvironment( $runtime->vmContext, is_string($queryString) ? $queryString : null, - is_string($postBody) ? $postBody : null + is_string($postBody) ? $postBody : null, + $scriptFilename ); $block = $runtime->parseAndCompile($code, $filename); if (! isset($options['-l'])) { diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 66ba038e..d5d307ff 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -1287,8 +1287,8 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/JIT/SuperglobalInit.php` **Warnings** (review for bootstrap subset): -- new Variable (line 121) -- new VMVariable (line 145) +- new Variable (line 123) +- new VMVariable (line 147) - 7 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/ValueEchoHelper.php` @@ -1501,11 +1501,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/Web/Superglobals.php` **Warnings** (review for bootstrap subset): -- new HashTable (line 411) -- new Variable (line 412) -- new Variable (line 431) -- new Variable (line 481) -- 22 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- new HashTable (line 420) +- new Variable (line 421) +- new Variable (line 440) +- new Variable (line 515) +- 23 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `src/macro_functions.php` diff --git a/lib/AOT/runtime/superglobals_refresh.c b/lib/AOT/runtime/superglobals_refresh.c index e867d8e9..286c7def 100644 --- a/lib/AOT/runtime/superglobals_refresh.c +++ b/lib/AOT/runtime/superglobals_refresh.c @@ -439,6 +439,37 @@ static void apply_scheme_and_port(__hashtable__ *server) set_string_key(server, "SERVER_PORT", port_buf); } +static void resolve_script_filename( + const char *script_name, + char *out, + size_t out_len +) { + const char *from_env = getenv("SCRIPT_FILENAME"); + + out[0] = '\0'; + if (NULL != from_env && '\0' != from_env[0]) { + strncpy(out, from_env, out_len - 1); + out[out_len - 1] = '\0'; + + return; + } + + { + const char *document_root = getenv("DOCUMENT_ROOT"); + size_t root_len; + + if (NULL == document_root || '\0' == document_root[0] + || NULL == script_name || '\0' == script_name[0]) { + return; + } + root_len = strlen(document_root); + while (root_len > 0 && '/' == document_root[root_len - 1]) { + root_len--; + } + snprintf(out, out_len, "%.*s%s", (int) root_len, document_root, script_name); + } +} + static void derive_path_info(const char *script_name, const char *request_uri, char *out, size_t out_len) { char path_buf[1024]; @@ -481,6 +512,7 @@ void __superglobals__refresh(void) const char *script_name = env_or_empty("SCRIPT_NAME"); const char *request_uri = getenv("REQUEST_URI"); char path_info[512]; + char script_filename[1024]; char request_uri_buf[1024]; if (NULL == request_uri || '\0' == request_uri[0]) { @@ -516,6 +548,10 @@ void __superglobals__refresh(void) set_string_key(sg_SERVER, "QUERY_STRING", query_string); set_string_key(sg_SERVER, "SCRIPT_NAME", script_name); set_string_key(sg_SERVER, "PHP_SELF", script_name); + resolve_script_filename(script_name, script_filename, sizeof(script_filename)); + if ('\0' != script_filename[0]) { + set_string_key(sg_SERVER, "SCRIPT_FILENAME", script_filename); + } set_string_key(sg_SERVER, "REQUEST_URI", request_uri); set_string_key(sg_SERVER, "GATEWAY_INTERFACE", "CGI/1.1"); set_string_key(sg_SERVER, "SERVER_SOFTWARE", "PHP-Compiler-AOT"); diff --git a/lib/JIT/SuperglobalInit.php b/lib/JIT/SuperglobalInit.php index 55b79d49..f50d8e44 100644 --- a/lib/JIT/SuperglobalInit.php +++ b/lib/JIT/SuperglobalInit.php @@ -16,12 +16,14 @@ final class SuperglobalInit /** @var array */ public static array $globals = []; - /** $_SERVER keys repopulated by __superglobals__refresh (issue #201, #235). */ + /** $_SERVER keys repopulated by __superglobals__refresh (issue #201, #235, #296, #302). */ private const RUNTIME_SERVER_KEYS = [ 'REQUEST_SCHEME', 'HTTPS', 'SERVER_PORT', 'SERVER_NAME', + 'DOCUMENT_ROOT', + 'SCRIPT_FILENAME', ]; public static function declareRefresh(Context $context): void diff --git a/lib/Web/DevServer.php b/lib/Web/DevServer.php index d24a8ca8..948c340e 100644 --- a/lib/Web/DevServer.php +++ b/lib/Web/DevServer.php @@ -105,6 +105,11 @@ public static function handleConnection($conn, string $docroot, callable $handle return; } + $scriptFilename = realpath($script); + if (false === $scriptFilename) { + $scriptFilename = $script; + } + $requestUri = $scriptName.$pathInfo; if ('' !== $query) { $requestUri .= '?'.$query; @@ -115,6 +120,7 @@ public static function handleConnection($conn, string $docroot, callable $handle 'QUERY_STRING' => $query, 'REQUEST_BODY' => $body, 'SCRIPT_NAME' => $scriptName, + 'SCRIPT_FILENAME' => $scriptFilename, 'REQUEST_URI' => $requestUri, 'DOCUMENT_ROOT' => $docroot, ]; @@ -129,6 +135,7 @@ public static function handleConnection($conn, string $docroot, callable $handle putenv('QUERY_STRING='.$query); putenv('REQUEST_BODY='.$body); putenv('SCRIPT_NAME='.$scriptName); + putenv('SCRIPT_FILENAME='.$scriptFilename); putenv('REQUEST_URI='.$requestUri); putenv('DOCUMENT_ROOT='.$docroot); if ('' !== $pathInfo) { diff --git a/lib/Web/Superglobals.php b/lib/Web/Superglobals.php index c3d91d62..39059afe 100644 --- a/lib/Web/Superglobals.php +++ b/lib/Web/Superglobals.php @@ -47,9 +47,13 @@ public static function isSuperglobalName(string $name): bool public static function populateFromEnvironment( Context $context, ?string $queryString = null, - ?string $postBody = null + ?string $postBody = null, + ?string $scriptFilename = null ): void { self::$activeContext = $context; + if (null !== $scriptFilename && '' !== $scriptFilename) { + putenv('SCRIPT_FILENAME='.$scriptFilename); + } if (null === $queryString) { $fromEnv = getenv('QUERY_STRING'); $queryString = false === $fromEnv ? '' : $fromEnv; @@ -161,6 +165,11 @@ private static function populateServer( self::setStringEntry($server, 'SCRIPT_NAME', $scriptName); self::setStringEntry($server, 'PHP_SELF', $scriptName); + $scriptFilename = self::resolveScriptFilename($scriptName); + if ('' !== $scriptFilename) { + self::setStringEntry($server, 'SCRIPT_FILENAME', $scriptFilename); + } + $requestUri = getenv('REQUEST_URI'); if (false === $requestUri || '' === $requestUri) { $requestUri = $scriptName; @@ -456,6 +465,31 @@ private static function setScalarEntry(HashTable $ht, $key, $value): void } } + /** + * Resolve absolute filesystem path for the entry script (issue #302). + */ + public static function resolveScriptFilename(?string $scriptName = null): string + { + $fromEnv = getenv('SCRIPT_FILENAME'); + if (false !== $fromEnv && '' !== $fromEnv) { + return $fromEnv; + } + + if (null === $scriptName) { + $scriptName = getenv('SCRIPT_NAME'); + if (false === $scriptName || '' === $scriptName) { + $scriptName = '/index.php'; + } + } + + $documentRoot = getenv('DOCUMENT_ROOT'); + if (false !== $documentRoot && '' !== $documentRoot) { + return rtrim($documentRoot, '/').$scriptName; + } + + return ''; + } + /** * Derive PATH_INFO from REQUEST_URI path suffix after SCRIPT_NAME (front-controller pattern). */ diff --git a/test/aot/RuntimeSuperglobalRefreshTest.php b/test/aot/RuntimeSuperglobalRefreshTest.php index 251b0cec..38d002b9 100644 --- a/test/aot/RuntimeSuperglobalRefreshTest.php +++ b/test/aot/RuntimeSuperglobalRefreshTest.php @@ -184,6 +184,60 @@ public function testCookieFromHttpCookieEnv(): void @unlink($outfile); } + public function testScriptFilenameFromCgiEnvironment(): void + { + $source = <<<'PHP' +assertNotFalse($outfile); + unlink($outfile); + + $repoRoot = dirname(__DIR__, 2); + $env = $this->llvmProcessEnv($repoRoot); + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $compile = proc_open( + array_merge( + self::llvmEnvPrefix(), + self::phpCommand(), + [$this->compileBin, '-o', $outfile] + ), + $descriptorSpec, + $pipes, + $repoRoot, + $env + ); + fwrite($pipes[0], $source); + fclose($pipes[0]); + $compileErr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($compile); + $this->assertFileExists($outfile, trim($compileErr !== false ? $compileErr : '')); + + $root = realpath(sys_get_temp_dir()); + $this->assertNotFalse($root); + $runEnv = $env; + unset($runEnv['SCRIPT_FILENAME']); + $runEnv['DOCUMENT_ROOT'] = $root; + $runEnv['SCRIPT_NAME'] = '/index.php'; + $runEnv['REQUEST_URI'] = '/index.php'; + $output = $this->runBinary($outfile, $runEnv); + $expected = $root.'/index.php'; + $this->assertStringContainsString($expected, $this->cgiBody($output)); + + @unlink($outfile); + } + public function testDocumentRootFromCgiEnvironment(): void { $source = <<<'PHP' @@ -287,6 +341,13 @@ public function testHttpHostFromCgiEnvironment(): void @unlink($outfile); } + private function cgiBody(string $output): string + { + $parts = preg_split("/\r?\n\r?\n/", $output, 2); + + return $parts[1] ?? $output; + } + /** * @param array $env */ diff --git a/test/fixtures/aot/cases/web_script_filename.phpt b/test/fixtures/aot/cases/web_script_filename.phpt new file mode 100644 index 00000000..68a3cfc7 --- /dev/null +++ b/test/fixtures/aot/cases/web_script_filename.phpt @@ -0,0 +1,15 @@ +--TEST-- +AOT: SCRIPT_FILENAME from CGI env (issue #302) +--ENV-- +REQUEST_METHOD=GET +SCRIPT_NAME=/index.php +REQUEST_URI=/index.php +DOCUMENT_ROOT=/var/www/html +SCRIPT_FILENAME=/var/www/html/index.php +--FILE-- +phpCmd = self::phpCommand(); } + public function testServeAotPopulatesScriptFilename(): void + { + $docroot = $this->makeDocroot([ + 'script.php' => <<<'PHP' +assertNotFalse($script); + $binaryDir = sys_get_temp_dir().'/phpc_serve_aot_sf_'.bin2hex(random_bytes(4)); + $this->assertTrue(mkdir($binaryDir)); + $binary = $binaryDir.'/app'; + $this->compileExample($docroot.'/script.php', $binary); + $response = $this->httpGetAot($docroot, $binary, '/script.php'); + $this->assertStringContainsString('HTTP/1.1 200', $response); + $this->assertStringContainsString($script, $response); + @unlink($binary); + @rmdir($binaryDir); + } + public function testServeAotPopulatesDocumentRoot(): void { $docroot = $this->makeDocroot([ diff --git a/test/real/ServeTest.php b/test/real/ServeTest.php index 55a9a338..b04e29e3 100644 --- a/test/real/ServeTest.php +++ b/test/real/ServeTest.php @@ -119,6 +119,21 @@ public function testPopulatesDocumentRoot(): void $this->assertStringContainsString($resolved, $response); } + public function testPopulatesScriptFilename(): void + { + $docroot = $this->makeDocroot([ + 'script.php' => <<<'PHP' +assertNotFalse($script); + $response = $this->httpGet($docroot, '/script.php'); + $this->assertStringContainsString('HTTP/1.1 200', $response); + $this->assertStringContainsString($script, $response); + } + public function testPopulatesCookieFromRequestHeader(): void { $docroot = $this->makeDocroot([ diff --git a/test/real/cases/web_script_filename.phpt b/test/real/cases/web_script_filename.phpt new file mode 100644 index 00000000..384922db --- /dev/null +++ b/test/real/cases/web_script_filename.phpt @@ -0,0 +1,15 @@ +--TEST-- +Web: SCRIPT_FILENAME from CGI env (issue #302) +--ENV-- +REQUEST_METHOD=GET +SCRIPT_NAME=/index.php +REQUEST_URI=/index.php +DOCUMENT_ROOT=/var/www/html +SCRIPT_FILENAME=/var/www/html/index.php +--FILE-- +assertStringContainsString('$_GET', $out !== false ? $out : ''); } + public function testRunPopulatesScriptFilename(): void + { + $repoRoot = dirname(__DIR__, 2); + $runner = $repoRoot.'/test/fixtures/web_echo_script_filename.php'; + $resolved = realpath($runner); + $this->assertNotFalse($resolved); + $cmd = array_merge( + self::phpCommand(), + [$repoRoot.'/bin/phpc.php', 'run', '-q', '', $runner] + ); + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $repoRoot); + $this->assertIsResource($proc); + fclose($pipes[0]); + $out = stream_get_contents($pipes[1]); + $err = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame(0, $exit, $err !== false ? $err : ''); + $this->assertStringContainsString($resolved, $out !== false ? $out : ''); + } + public function testRunSimpleWebWithQueryFlag(): void { $repoRoot = dirname(__DIR__, 2); diff --git a/test/unit/Web/SuperglobalsScriptFilenameTest.php b/test/unit/Web/SuperglobalsScriptFilenameTest.php new file mode 100644 index 00000000..96f86464 --- /dev/null +++ b/test/unit/Web/SuperglobalsScriptFilenameTest.php @@ -0,0 +1,49 @@ +assertSame('/var/www/html/index.php', Superglobals::resolveScriptFilename()); + } + + public function testResolveScriptFilenameJoinsDocumentRootAndScriptName(): void + { + putenv('SCRIPT_FILENAME'); + putenv('DOCUMENT_ROOT=/var/www/html'); + $this->assertSame( + '/var/www/html/app/index.php', + Superglobals::resolveScriptFilename('/app/index.php') + ); + } + + public function testPopulateSetsScriptFilenameOnServerSuperglobal(): void + { + putenv('SCRIPT_FILENAME=/var/www/html/index.php'); + $runtime = new Runtime(); + Superglobals::populateFromEnvironment($runtime->vmContext, '', ''); + + $server = $runtime->vmContext->getSuperglobal('_SERVER')->toArray(); + $var = $server->find('SCRIPT_FILENAME'); + $this->assertNotNull($var); + $this->assertSame('/var/www/html/index.php', $var->resolveIndirect()->toString()); + } +}