diff --git a/phpunit/src/ServerEnvironmentTest.php b/phpunit/src/ServerEnvironmentTest.php new file mode 100644 index 00000000..38f2b1e1 --- /dev/null +++ b/phpunit/src/ServerEnvironmentTest.php @@ -0,0 +1,73 @@ +testDir = sys_get_temp_dir() . '/server_environment_test_' . uniqid(); + mkdir($this->testDir, 0777, true); + } + + protected function tearDown(): void + { + parent::tearDown(); + $this->removeDirectory($this->testDir); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + foreach (array_diff(scandir($dir), ['.', '..']) as $file) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } + + public function testGeneratedServerEnvironmentMatchesCliAndEscapesScriptPath(): void + { + $compiler = CompilerTest::create($this->testDir); + $method = new ReflectionMethod(Translator::class, 'registerServerEnvironment'); + $code = $method->invoke($compiler, 'C:\\project\\"quoted"\\main.php'); + + $this->assertStringContainsString('php::Var &_SERVER = _global_var__SERVER;', $code); + $this->assertStringContainsString('php::Str php_self = "PHP_SELF";', $code); + $this->assertStringContainsString('php::Str script_name = "SCRIPT_NAME";', $code); + $this->assertStringContainsString('php::Str script_filename = "SCRIPT_FILENAME";', $code); + $this->assertStringContainsString('php::Str path_translated = "PATH_TRANSLATED";', $code); + $this->assertStringContainsString('php::Str document_root = "DOCUMENT_ROOT";', $code); + $this->assertStringContainsString( + 'php::Str value = "' . $compiler->escapeString('C:\\project\\"quoted"\\main.php') . '";', + $code + ); + $this->assertStringContainsString('_SERVER.item(path_translated, true) = value;', $code); + $this->assertStringContainsString('_SERVER.item(document_root, true) = "";', $code); + } + + public function testServerGlobalIsForcedOnlyForBinaryBuilds(): void + { + $binaryCompiler = CompilerTest::create($this->testDir); + $binaryFile = $this->testDir . '/binary.h'; + $binaryCompiler->genDataDeclarations($binaryFile); + $this->assertStringContainsString('_global_var__SERVER', file_get_contents($binaryFile)); + + $extensionCompiler = CompilerTest::create($this->testDir); + $extensionCompiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $extensionFile = $this->testDir . '/extension.h'; + $extensionCompiler->genDataDeclarations($extensionFile); + $this->assertStringNotContainsString('_global_var__SERVER', file_get_contents($extensionFile)); + } +} diff --git a/src/Translator.php b/src/Translator.php index 67f5d479..6aa5a7b8 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -690,6 +690,13 @@ class Translator extends Preprocessor { $lines[] = '#include '; $lines[] = PHP_EOL; + + // Embedded binaries populate the CLI script fields in $_SERVER at + // request startup, even when the source does not reference $_SERVER. + if ($this->isBuildModeBin() && !$this->hasGlobalVar('_SERVER')) { + $this->addGlobalVar('_SERVER', Type::ARRAY); + } + foreach ($this->globalVars as $name => $type) { $lines[] = 'extern THREAD_LOCAL ' . Type::VAR . ' ' . $this->escapeGlobalVar($name) . ';'; } @@ -1038,7 +1045,10 @@ CODE; if ($this->isBuildModeBin()) { $entryFunction = $this->symbols->function(self::ENTRY_FUNCTION); + // FunctionDef::sourceFile comes from loadFile()'s realpath(), so the + // CLI script fields always identify main()'s canonical absolute file. $entryFile = $entryFunction->sourceFile; + $code .= $this->registerServerEnvironment($entryFile); $entryFileArg = $this->genCharPtr($entryFile, true); $entryPrefix = str_repeat("\n", max(0, $entryFunction->startLine - 1)); if (count($entryFunction->argInfoList) == 2) { @@ -1046,6 +1056,7 @@ CODE; } else { $entryScript = $entryPrefix . 'main();'; } + $code .= 'php::eval(' . $this->genCharPtr($entryScript, true) . ', ' . $entryFileArg . ');' . PHP_EOL; } @@ -3222,6 +3233,25 @@ CODE; return $cppCode; } + private function registerServerEnvironment(string $entryFile): string + { + $cppCode = 'php::Var &_SERVER = ' . $this->escapeGlobalVar('_SERVER') . ';' . PHP_EOL; + $cppCode .= 'php::Str php_self = "PHP_SELF";' . PHP_EOL; + $cppCode .= 'php::Str script_name = "SCRIPT_NAME";' . PHP_EOL; + $cppCode .= 'php::Str script_filename = "SCRIPT_FILENAME";' . PHP_EOL; + $cppCode .= 'php::Str path_translated = "PATH_TRANSLATED";' . PHP_EOL; + $cppCode .= 'php::Str document_root = "DOCUMENT_ROOT";' . PHP_EOL; + $cppCode .= 'php::Str value = ' . $this->genCharPtr($entryFile, true) . ';' . PHP_EOL; + + $cppCode .= '_SERVER.item(php_self, true) = value;' . PHP_EOL; + $cppCode .= '_SERVER.item(script_name, true) = value;' . PHP_EOL; + $cppCode .= '_SERVER.item(script_filename, true) = value;' . PHP_EOL; + $cppCode .= '_SERVER.item(path_translated, true) = value;' . PHP_EOL; + $cppCode .= '_SERVER.item(document_root, true) = "";' . PHP_EOL; + + return $cppCode . PHP_EOL; + } + private function canConsumeForwardedArgument(ArgInfo $argInfo): bool { if ($argInfo->byRef) { diff --git a/tests/compiler/super_global/server-init.phpt b/tests/compiler/super_global/server-init.phpt new file mode 100644 index 00000000..ce8cc049 --- /dev/null +++ b/tests/compiler/super_global/server-init.phpt @@ -0,0 +1,24 @@ +--TEST-- +$_SERVER: init PHP_SELF, SCRIPT_NAME, SCRIPT_FILENAME, PATH_TRANSLATED and DOCUMENT_ROOT +--ENV-- +PHP_SELF=from-environment +SCRIPT_NAME=from-environment +SCRIPT_FILENAME=from-environment +PATH_TRANSLATED=from-environment +DOCUMENT_ROOT=from-environment +--FILE-- + +--EXPECT--