You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
8.2 KiB
358 lines
8.2 KiB
<?php |
|
|
|
/** |
|
* 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 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 !== null) { |
|
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 === true && ($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; |
|
} |
|
|
|
/** |
|
* Disables cache configuration. |
|
* |
|
* @throws DownloadException |
|
*/ |
|
public function setCache(array $options = []) |
|
{ |
|
throw DownloadException::forCannotSetCache(); |
|
} |
|
|
|
/** |
|
* {@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('Expires-Disposition', '0'); |
|
$this->setHeader('Content-Transfer-Encoding', 'binary'); |
|
$this->setHeader('Content-Length', (string) $this->getContentLength()); |
|
$this->noCache(); |
|
} |
|
|
|
/** |
|
* output download file text. |
|
* |
|
* @return DownloadResponse |
|
* |
|
* @throws DownloadException |
|
*/ |
|
public function sendBody() |
|
{ |
|
if ($this->binary !== null) { |
|
return $this->sendBodyByBinary(); |
|
} |
|
|
|
if ($this->file !== null) { |
|
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; |
|
} |
|
}
|
|
|