mirror of
https://github.com/codeigniter4/CodeIgniter4.git
synced 2025-02-20 11:44:28 +08:00
349 lines
7.9 KiB
PHP
349 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* This file is part of CodeIgniter 4 framework.
|
|
*
|
|
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace CodeIgniter\HTTP;
|
|
|
|
use CodeIgniter\Exceptions\DownloadException;
|
|
use CodeIgniter\Files\File;
|
|
use Config\App;
|
|
use Config\Mimes;
|
|
|
|
/**
|
|
* HTTP response when a download is requested.
|
|
*
|
|
* @see \CodeIgniter\HTTP\DownloadResponseTest
|
|
*/
|
|
class DownloadResponse extends Response
|
|
{
|
|
/**
|
|
* Download file name
|
|
*/
|
|
private string $filename;
|
|
|
|
/**
|
|
* Download for file
|
|
*/
|
|
private ?File $file = null;
|
|
|
|
/**
|
|
* mime set flag
|
|
*/
|
|
private readonly bool $setMime;
|
|
|
|
/**
|
|
* Download for binary
|
|
*/
|
|
private ?string $binary = null;
|
|
|
|
/**
|
|
* Download charset
|
|
*/
|
|
private string $charset = 'UTF-8';
|
|
|
|
/**
|
|
* Download reason
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $reason = 'OK';
|
|
|
|
/**
|
|
* The current status code for this response.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $statusCode = 200;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct(string $filename, bool $setMime)
|
|
{
|
|
parent::__construct(config(App::class));
|
|
|
|
$this->filename = $filename;
|
|
$this->setMime = $setMime;
|
|
|
|
// Make sure the content type is either specified or detected
|
|
$this->removeHeader('Content-Type');
|
|
}
|
|
|
|
/**
|
|
* set download for binary string.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setBinary(string $binary)
|
|
{
|
|
if ($this->file instanceof File) {
|
|
throw DownloadException::forCannotSetBinary();
|
|
}
|
|
|
|
$this->binary = $binary;
|
|
}
|
|
|
|
/**
|
|
* set download for file.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setFilePath(string $filepath)
|
|
{
|
|
if ($this->binary !== null) {
|
|
throw DownloadException::forCannotSetFilePath($filepath);
|
|
}
|
|
|
|
$this->file = new File($filepath, true);
|
|
}
|
|
|
|
/**
|
|
* set name for the download.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setFileName(string $filename)
|
|
{
|
|
$this->filename = $filename;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* get content length.
|
|
*/
|
|
public function getContentLength(): int
|
|
{
|
|
if (is_string($this->binary)) {
|
|
return strlen($this->binary);
|
|
}
|
|
|
|
if ($this->file instanceof File) {
|
|
return $this->file->getSize();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Set content type by guessing mime type from file extension
|
|
*/
|
|
private function setContentTypeByMimeType(): void
|
|
{
|
|
$mime = null;
|
|
$charset = '';
|
|
|
|
if ($this->setMime && ($lastDotPosition = strrpos($this->filename, '.')) !== false) {
|
|
$mime = Mimes::guessTypeFromExtension(substr($this->filename, $lastDotPosition + 1));
|
|
$charset = $this->charset;
|
|
}
|
|
|
|
if (! is_string($mime)) {
|
|
// Set the default MIME type to send
|
|
$mime = 'application/octet-stream';
|
|
$charset = '';
|
|
}
|
|
|
|
$this->setContentType($mime, $charset);
|
|
}
|
|
|
|
/**
|
|
* get download filename.
|
|
*/
|
|
private function getDownloadFileName(): string
|
|
{
|
|
$filename = $this->filename;
|
|
$x = explode('.', $this->filename);
|
|
$extension = end($x);
|
|
|
|
/* It was reported that browsers on Android 2.1 (and possibly older as well)
|
|
* need to have the filename extension upper-cased in order to be able to
|
|
* download it.
|
|
*
|
|
* Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/
|
|
*/
|
|
// @todo: depend super global
|
|
if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT'])
|
|
&& preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) {
|
|
$x[count($x) - 1] = strtoupper($extension);
|
|
$filename = implode('.', $x);
|
|
}
|
|
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* get Content-Disposition Header string.
|
|
*/
|
|
private function getContentDisposition(): string
|
|
{
|
|
$downloadFilename = $this->getDownloadFileName();
|
|
|
|
$utf8Filename = $downloadFilename;
|
|
|
|
if (strtoupper($this->charset) !== 'UTF-8') {
|
|
$utf8Filename = mb_convert_encoding($downloadFilename, 'UTF-8', $this->charset);
|
|
}
|
|
|
|
$result = sprintf('attachment; filename="%s"', $downloadFilename);
|
|
|
|
if ($utf8Filename !== '') {
|
|
$result .= '; filename*=UTF-8\'\'' . rawurlencode($utf8Filename);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Disallows status changing.
|
|
*
|
|
* @throws DownloadException
|
|
*/
|
|
public function setStatusCode(int $code, string $reason = '')
|
|
{
|
|
throw DownloadException::forCannotSetStatusCode($code, $reason);
|
|
}
|
|
|
|
/**
|
|
* Sets the Content Type header for this response with the mime type
|
|
* and, optionally, the charset.
|
|
*
|
|
* @return ResponseInterface
|
|
*/
|
|
public function setContentType(string $mime, string $charset = 'UTF-8')
|
|
{
|
|
parent::setContentType($mime, $charset);
|
|
|
|
if ($charset !== '') {
|
|
$this->charset = $charset;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Sets the appropriate headers to ensure this response
|
|
* is not cached by the browsers.
|
|
*/
|
|
public function noCache(): self
|
|
{
|
|
$this->removeHeader('Cache-Control');
|
|
$this->setHeader('Cache-Control', ['private', 'no-transform', 'no-store', 'must-revalidate']);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* @return $this
|
|
*
|
|
* @todo Do downloads need CSP or Cookies? Compare with ResponseTrait::send()
|
|
*/
|
|
public function send()
|
|
{
|
|
// Turn off output buffering completely, even if php.ini output_buffering is not off
|
|
if (ENVIRONMENT !== 'testing') {
|
|
while (ob_get_level() > 0) {
|
|
ob_end_clean();
|
|
}
|
|
}
|
|
|
|
$this->buildHeaders();
|
|
$this->sendHeaders();
|
|
$this->sendBody();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* set header for file download.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function buildHeaders()
|
|
{
|
|
if (! $this->hasHeader('Content-Type')) {
|
|
$this->setContentTypeByMimeType();
|
|
}
|
|
|
|
if (! $this->hasHeader('Content-Disposition')) {
|
|
$this->setHeader('Content-Disposition', $this->getContentDisposition());
|
|
}
|
|
|
|
$this->setHeader('Content-Transfer-Encoding', 'binary');
|
|
$this->setHeader('Content-Length', (string) $this->getContentLength());
|
|
}
|
|
|
|
/**
|
|
* output download file text.
|
|
*
|
|
* @return DownloadResponse
|
|
*
|
|
* @throws DownloadException
|
|
*/
|
|
public function sendBody()
|
|
{
|
|
if ($this->binary !== null) {
|
|
return $this->sendBodyByBinary();
|
|
}
|
|
|
|
if ($this->file instanceof File) {
|
|
return $this->sendBodyByFilePath();
|
|
}
|
|
|
|
throw DownloadException::forNotFoundDownloadSource();
|
|
}
|
|
|
|
/**
|
|
* output download text by file.
|
|
*
|
|
* @return DownloadResponse
|
|
*/
|
|
private function sendBodyByFilePath()
|
|
{
|
|
$splFileObject = $this->file->openFile('rb');
|
|
|
|
// Flush 1MB chunks of data
|
|
while (! $splFileObject->eof() && ($data = $splFileObject->fread(1_048_576)) !== false) {
|
|
echo $data;
|
|
unset($data);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* output download text by binary
|
|
*
|
|
* @return DownloadResponse
|
|
*/
|
|
private function sendBodyByBinary()
|
|
{
|
|
echo $this->binary;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Sets the response header to display the file in the browser.
|
|
*
|
|
* @return DownloadResponse
|
|
*/
|
|
public function inline()
|
|
{
|
|
$this->setHeader('Content-Disposition', 'inline');
|
|
|
|
return $this;
|
|
}
|
|
}
|