mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
167 lines
4.0 KiB
PHP
167 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace SilverStripe\Control;
|
|
|
|
use BadMethodCallException;
|
|
|
|
/**
|
|
* A response which contains a streamable data source.
|
|
*
|
|
* @package framework
|
|
* @subpackage control
|
|
*/
|
|
class HTTPStreamResponse extends HTTPResponse
|
|
{
|
|
|
|
/**
|
|
* Stream source for this response
|
|
*
|
|
* @var resource
|
|
*/
|
|
protected $stream = null;
|
|
|
|
/**
|
|
* Set to true if this stream has been consumed.
|
|
* A consumed non-seekable stream will not be re-consumable
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $consumed = false;
|
|
|
|
/**
|
|
* HTTPStreamResponse constructor.
|
|
* @param resource $stream Data stream
|
|
* @param int $contentLength size of the stream in bytes
|
|
* @param int $statusCode The numeric status code - 200, 404, etc
|
|
* @param string $statusDescription The text to be given alongside the status code.
|
|
*/
|
|
public function __construct($stream, $contentLength, $statusCode = null, $statusDescription = null)
|
|
{
|
|
parent::__construct(null, $statusCode, $statusDescription);
|
|
$this->setStream($stream);
|
|
if ($contentLength) {
|
|
$this->addHeader('Content-Length', $contentLength);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if a stream is seekable
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function isSeekable()
|
|
{
|
|
$stream = $this->getStream();
|
|
if (!$stream) {
|
|
return false;
|
|
}
|
|
$metadata = stream_get_meta_data($stream);
|
|
return $metadata['seekable'];
|
|
}
|
|
|
|
/**
|
|
* @return resource
|
|
*/
|
|
public function getStream()
|
|
{
|
|
return $this->stream;
|
|
}
|
|
|
|
/**
|
|
* @param resource $stream
|
|
* @return $this
|
|
*/
|
|
public function setStream($stream)
|
|
{
|
|
$this->setBody(null);
|
|
$this->stream = $stream;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get body prior to stream traversal
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getSavedBody()
|
|
{
|
|
return parent::getBody();
|
|
}
|
|
|
|
public function getBody()
|
|
{
|
|
$body = $this->getSavedBody();
|
|
if (isset($body)) {
|
|
return $body;
|
|
}
|
|
|
|
// Consume stream into string
|
|
$body = $this->consumeStream(function ($stream) {
|
|
$body = stream_get_contents($stream);
|
|
|
|
// If this stream isn't seekable, we'll need to save the body
|
|
// in case of subsequent requests.
|
|
if (!$this->isSeekable()) {
|
|
$this->setBody($body);
|
|
}
|
|
return $body;
|
|
});
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Safely consume the stream
|
|
*
|
|
* @param callable $callback Callback which will perform the consumable action on the stream
|
|
* @return mixed Result of $callback($stream) or null if no stream available
|
|
* @throws BadMethodCallException Throws exception if stream can't be re-consumed
|
|
*/
|
|
protected function consumeStream($callback)
|
|
{
|
|
// Load from stream
|
|
$stream = $this->getStream();
|
|
if (!$stream) {
|
|
return null;
|
|
}
|
|
|
|
// Check if stream must be rewound
|
|
if ($this->consumed) {
|
|
if (!$this->isSeekable()) {
|
|
throw new BadMethodCallException(
|
|
"Unseekable stream has already been consumed"
|
|
);
|
|
}
|
|
rewind($stream);
|
|
}
|
|
|
|
// Consume
|
|
$this->consumed = true;
|
|
return $callback($stream);
|
|
}
|
|
|
|
/**
|
|
* Output body of this response to the browser
|
|
*/
|
|
protected function outputBody()
|
|
{
|
|
// If the output has been overwritten, or the stream is irreversable and has
|
|
// already been consumed, return the cached body.
|
|
$body = $this->getSavedBody();
|
|
if ($body) {
|
|
echo $body;
|
|
return;
|
|
}
|
|
|
|
// Stream to output
|
|
if ($this->getStream()) {
|
|
$this->consumeStream(function ($stream) {
|
|
fpassthru($stream);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Fail over
|
|
parent::outputBody();
|
|
}
|
|
}
|