diff --git a/src/main/php/web/Response.class.php b/src/main/php/web/Response.class.php index e9842bdd..1a6a18f3 100755 --- a/src/main/php/web/Response.class.php +++ b/src/main/php/web/Response.class.php @@ -12,6 +12,7 @@ */ class Response { private $output; + private $streaming= null; private $flushed= false; private $status= 200; private $message= 'OK'; @@ -109,13 +110,30 @@ public function headers() { return $r; } - /** @param web.io.Output $output */ + /** + * Begins output by sending the status line and headers + * + * @param web.io.Output $output + * @return web.io.Output + */ private function begin($output) { $output->begin($this->status, $this->message, $this->cookies - ? array_merge($this->headers, ['Set-Cookie' => array_map(function($c) { return $c->header(); }, $this->cookies)]) + ? $this->headers + ['Set-Cookie' => array_map(function($c) { return $c->header(); }, $this->cookies)] : $this->headers ); $this->flushed= true; + return $output; + } + + /** + * Changes the implementation used inside `stream()` to determine the output. + * + * @param function(self, ?int): web.io.Output $implementation + * @return self + */ + public function streaming(callable $implementation) { + $this->streaming= $implementation; + return $this; } /** @@ -129,7 +147,7 @@ public function flush() { throw new IllegalStateException('Response already flushed'); } - $this->begin($this->output); + $this->begin($this->streaming ? ($this->streaming)($this, 0) : $this->output); } /** @@ -152,26 +170,20 @@ public function end() { } /** - * Returns a stream to write on + * Returns a stream to write on: When given a length, sets the `Content-Length` + * header to this value and writes the subsequently given bytes unmodified to + * the output. Otheriwse, chunked transfer encoding is used. * - * @param int $size If omitted, uses chunked transfer encoding + * @param ?int $length * @return io.streams.OutputStream * @throws lang.IllegalStateException */ - public function stream($size= null) { + public function stream($length= null) { if ($this->flushed) { throw new IllegalStateException('Response already flushed'); } - if (null === $size) { - $output= $this->output->stream(); - } else { - $this->headers['Content-Length']= [$size]; - $output= $this->output; - } - - $this->begin($output); - return $output; + return $this->begin($this->streaming ? ($this->streaming)($this, $length) : $this->output->stream($length)); } /** diff --git a/src/main/php/web/io/Buffered.class.php b/src/main/php/web/io/Buffered.class.php index eadabd24..caa8dce3 100755 --- a/src/main/php/web/io/Buffered.class.php +++ b/src/main/php/web/io/Buffered.class.php @@ -1,14 +1,12 @@ target= $target; } diff --git a/src/main/php/web/io/Output.class.php b/src/main/php/web/io/Output.class.php index 85e8bc1f..2d1e8da1 100755 --- a/src/main/php/web/io/Output.class.php +++ b/src/main/php/web/io/Output.class.php @@ -36,9 +36,10 @@ public abstract function write($bytes); * Returns an output used when the content-length is not known at the * time of starting the output. * + * @param ?int $length * @return self */ - public function stream() { return $this; } + public function stream($length= null) { return $this; } /** @return void */ public function finish() { } @@ -47,7 +48,7 @@ public function finish() { } public function flush() { } /** @return void */ - public function close() { + public final function close() { if ($this->closed) return; $this->finish(); $this->closed= true; diff --git a/src/main/php/web/io/TestOutput.class.php b/src/main/php/web/io/TestOutput.class.php index 9cfc32dd..23dabe6e 100755 --- a/src/main/php/web/io/TestOutput.class.php +++ b/src/main/php/web/io/TestOutput.class.php @@ -5,7 +5,7 @@ /** * Input for testing purposes * - * @test xp://web.unittest.io.TestOutputTest + * @test web.unittest.io.TestOutputTest */ class TestOutput extends Output { private $stream; @@ -53,8 +53,15 @@ public function begin($status, $message, $headers) { $this->bytes.= "\r\n"; } - /** @return web.io.Output */ - public function stream() { return $this->stream->newInstance($this); } + /** + * Returns writer with length if known, using the configured stream otherwise + * + * @param ?int $length + * @return web.io.Output + */ + public function stream($length= null) { + return null === $length ? $this->stream->newInstance($this) : new WriteLength($this, $length); + } /** * Writes the bytes (in this case, to the internal buffer which can be diff --git a/src/main/php/web/io/WriteChunks.class.php b/src/main/php/web/io/WriteChunks.class.php index f7a43fe9..b5d5128f 100755 --- a/src/main/php/web/io/WriteChunks.class.php +++ b/src/main/php/web/io/WriteChunks.class.php @@ -4,15 +4,15 @@ * Writes Chunked transfer encoding * * @see https://tools.ietf.org/html/rfc7230#section-4.1 - * @test xp://web.unittest.io.WriteChunksTest + * @test web.unittest.io.WriteChunksTest */ class WriteChunks extends Output { - const BUFFER_SIZE = 4096; + const BUFFER_SIZE= 4096; private $target; private $buffer= ''; - /** @param web.io.Output $target */ + /** @param parent $target */ public function __construct($target) { $this->target= $target; } diff --git a/src/main/php/web/io/WriteLength.class.php b/src/main/php/web/io/WriteLength.class.php new file mode 100755 index 00000000..e571a1d4 --- /dev/null +++ b/src/main/php/web/io/WriteLength.class.php @@ -0,0 +1,54 @@ +target= $target; + $this->length= $length; + } + + /** + * Begins output + * + * @param int $status + * @param string $message + * @param [:string] $headers + * @return void + */ + public function begin($status, $message, $headers) { + $headers['Content-Length']= [$this->length]; + $this->target->begin($status, $message, $headers); + } + + /** + * Writes a chunk of data + * + * @param string $chunk + * @return void + */ + public function write($chunk) { + $this->target->write($chunk); + } + + /** @return void */ + public function flush() { + $this->target->flush(); + } + + /** @return void */ + public function finish() { + $this->target->finish(); + } +} \ No newline at end of file diff --git a/src/main/php/xp/web/SAPI.class.php b/src/main/php/xp/web/SAPI.class.php index 19746e7d..abf7da7a 100755 --- a/src/main/php/xp/web/SAPI.class.php +++ b/src/main/php/xp/web/SAPI.class.php @@ -1,6 +1,6 @@ status= $status; - $this->message= $message; - $this->headers= $headers; - } - - public function write($bytes) { - $this->bytes.= $bytes; - } - - /** - * Drain this buffered output to a given output instance, closing it - * once finished. - * - * @param web.Response $res - * @return void - */ - public function drain($res) { - $res->answer($this->status, $this->message); - foreach ($this->headers as $name => $value) { - $res->header($name, $value); - } - - if ('' !== $this->bytes) { - $out= $res->stream($this->headers['Content-Length'][0] ?? null); - try { - $out->write($this->bytes); - } finally { - $out->close(); - } - } - } -} \ No newline at end of file diff --git a/src/main/php/xp/web/dev/CaptureOutput.class.php b/src/main/php/xp/web/dev/CaptureOutput.class.php new file mode 100755 index 00000000..001a9b58 --- /dev/null +++ b/src/main/php/xp/web/dev/CaptureOutput.class.php @@ -0,0 +1,71 @@ +length= $length; + return $this; + } + + /** + * Begins output + * + * @param int $status + * @param string $message + * @param [:string] $headers + * @return void + */ + public function begin($status, $message, $headers) { + $this->status= $status; + $this->message= $message; + $this->headers= $headers; + } + + /** + * Writes a chunk of data + * + * @param string $chunk + * @return void + */ + public function write($bytes) { + $this->bytes.= $bytes; + } + + /** + * Ensure response is flushed + * + * @param web.Response $response + * @return void + */ + public function end($response) { + if (-1 === $this->length) $response->flush(); + } + + /** + * Drain this buffered output to a given output instance, closing it + * once finished. + * + * @param web.Response $response + * @return void + */ + public function drain($response) { + $out= $response->output()->stream($this->length); + try { + $out->begin($this->status, $this->message, $this->headers); + $out->write($this->bytes); + } finally { + $out->close(); + } + } +} \ No newline at end of file diff --git a/src/main/php/xp/web/dev/Console.class.php b/src/main/php/xp/web/dev/Console.class.php index 1d9beeb4..5ecc879c 100755 --- a/src/main/php/xp/web/dev/Console.class.php +++ b/src/main/php/xp/web/dev/Console.class.php @@ -1,6 +1,8 @@ $value) { - $r.= ' - '.htmlspecialchars($name).' - '.htmlspecialchars(implode(', ', $value)).' - '; - } - return $r; + private function transform($template, $context) { + return preg_replace_callback( + '/\{\{([^ }]+) ?([^}]+)?\}\}/', + function($m) use($context) { + $value= $context[$m[1]] ?? ''; + return $value instanceof Closure ? $value($context[$m[2]] ?? '') : htmlspecialchars($value); + }, + $template + ); } /** @@ -45,30 +48,51 @@ private function rows($headers) { * @return var */ public function filter($req, $res, $invocation) { - $buffer= new Response(new Buffer()); - + $capture= new CaptureOutput(); try { ob_start(); - yield from $invocation->proceed($req, $buffer); - } finally { - $buffer->end(); + yield from $invocation->proceed($req, $res->streaming(function($res, $length) use($capture) { + return $capture->length($length); + })); + + $kind= 'Debug'; $debug= ob_get_clean(); + if (0 === strlen($debug)) return $capture->drain($res); + } catch (Any $e) { + $kind= 'Error'; + $res->answer($e instanceof Error ? $e->status() : 500); + $debug= ob_get_clean()."\n".Throwable::wrap($e)->toString(); + } finally { + $capture->end($res); } - $res->trace= $buffer->trace; - $out= $buffer->output(); - if (0 === strlen($debug)) { - $out->drain($res); - } else { - $res->status(200, 'Debug'); - $res->send(sprintf( - typeof($this)->getClassLoader()->getResource($this->template), - htmlspecialchars($debug), - $out->status, - htmlspecialchars($out->message), - $this->rows($out->headers), - htmlspecialchars($out->bytes) - )); + $console= $this->transform(typeof($this)->getClassLoader()->getResource($this->template), [ + 'kind' => $kind, + 'debug' => $debug, + 'status' => $capture->status, + 'message' => $capture->message, + 'headers' => $capture->headers, + 'contents' => $capture->bytes, + '#rows' => function($headers) { + $r= ''; + foreach ($headers as $name => $value) { + $r.= ' + '.htmlspecialchars($name).' + '.htmlspecialchars(implode(', ', $value)).' + '; + } + return $r; + } + ]); + $target= $res->output()->stream(strlen($console)); + try { + $target->begin(200, 'Debug', [ + 'Content-Type' => ['text/html; charset='.\xp::ENCODING], + 'Cache-Control' => ['no-cache, no-store'], + ]); + $target->write($console); + } finally { + $target->close(); } } } \ No newline at end of file diff --git a/src/main/php/xp/web/dev/console.html b/src/main/php/xp/web/dev/console.html index 7ff8e308..e62d68bd 100755 --- a/src/main/php/xp/web/dev/console.html +++ b/src/main/php/xp/web/dev/console.html @@ -1,12 +1,12 @@ - Debug + {{kind}}