|
9 | 9 | /** |
10 | 10 | * help() is deprecated: it prints links to the online docs, plain (no <xmp>) |
11 | 11 | * on CLI. The <xmp> wrap only happens for text/html web responses, which |
12 | | - * can't be simulated in-process (xmpWrap reads PHP_SAPI). |
| 12 | + * can't be simulated in-process (xmpWrap reads PHP_SAPI); one test reaches |
| 13 | + * it through PHP's built-in server. |
13 | 14 | * |
14 | 15 | * n/a dimensions: encoding, global settings, immutability, argument matrix |
15 | 16 | * ($value passes through untouched by design). |
@@ -39,4 +40,53 @@ public function testHelpReturnsValuePassthroughOnBothCallForms(): void |
39 | 40 | $this->assertSame('original value', $instanceResult); |
40 | 41 | $this->assertSame($staticOutput, $instanceOutput); |
41 | 42 | } |
| 43 | + |
| 44 | + /** |
| 45 | + * The <xmp> web branch is reachable under PHP's built-in server (SAPI |
| 46 | + * cli-server), so this is the one test that asserts the wrapped path: a |
| 47 | + * literal </xmp> can't end the block early - it displays as <\/xmp>, the |
| 48 | + * same escaping as CMSB's xmp_safe(). |
| 49 | + */ |
| 50 | + public function testXmpWrapEscapesXmpClosingTagOnWebResponses(): void |
| 51 | + { |
| 52 | + $body = $this->requestViaBuiltInServer('xmp-breakout.php'); |
| 53 | + |
| 54 | + $this->assertStringContainsString('<xmp>', $body); |
| 55 | + $this->assertStringContainsString('<\/xmp><script>alert(1)</script>', $body, 'payload displays escaped'); |
| 56 | + $this->assertSame(1, substr_count($body, '</xmp>'), 'only the wrapper itself closes the block'); |
| 57 | + } |
| 58 | + |
| 59 | + /** |
| 60 | + * Serve one Support/bin script through php -S and return the response |
| 61 | + * body. The built-in server is the one place tests can reach xmpWrap()'s |
| 62 | + * web branch (PHP_SAPI is 'cli' everywhere else in the suite). |
| 63 | + */ |
| 64 | + private function requestViaBuiltInServer(string $script): string |
| 65 | + { |
| 66 | + $docRoot = dirname(__DIR__) . '/Support/bin'; |
| 67 | + |
| 68 | + // find a free port, then hand it to php -S (it can't pick its own) |
| 69 | + $socket = stream_socket_server('tcp://127.0.0.1:0'); |
| 70 | + $this->assertNotFalse($socket, 'could not find a free port'); |
| 71 | + $port = (int)substr(strrchr(stream_socket_get_name($socket, false), ':'), 1); |
| 72 | + fclose($socket); |
| 73 | + |
| 74 | + $pipes = []; |
| 75 | + $server = proc_open([PHP_BINARY, '-S', "127.0.0.1:$port", '-t', $docRoot], [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); |
| 76 | + $this->assertIsResource($server, 'could not start php -S'); |
| 77 | + |
| 78 | + try { |
| 79 | + $context = stream_context_create(['http' => ['timeout' => 1]]); |
| 80 | + $body = false; |
| 81 | + for ($attempt = 0; $attempt < 50 && $body === false; $attempt++) { |
| 82 | + usleep(100_000); |
| 83 | + $body = @file_get_contents("http://127.0.0.1:$port/$script", false, $context); |
| 84 | + } |
| 85 | + $this->assertIsString($body, 'no response from php -S after 5 seconds'); |
| 86 | + return $body; |
| 87 | + } finally { |
| 88 | + proc_terminate($server); |
| 89 | + proc_close($server); |
| 90 | + } |
| 91 | + } |
42 | 92 | } |
0 commit comments