Newer
Older
framework / system / HTTP / DownloadResponse.php
@MGatner MGatner on 1 Feb 2021 7 KB Release v4.0.5
<?php

/**
 * This file is part of the 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\Mimes;
use DateTime;
use DateTimeZone;

/**
 * HTTP response when a download is requested.
 */
class DownloadResponse extends Response
{
	/**
	 * Download file name
	 *
	 * @var string
	 */
	private $filename;

	/**
	 * Download for file
	 *
	 * @var File|null
	 */
	private $file;

	/**
	 * mime set flag
	 *
	 * @var boolean
	 */
	private $setMime;

	/**
	 * Download for binary
	 *
	 * @var string|null
	 */
	private $binary;

	/**
	 * Download charset
	 *
	 * @var string
	 */
	private $charset = 'UTF-8';

	/**
	 * Download reason
	 *
	 * @var string
	 */
	protected $reason = 'OK';

	/**
	 * The current status code for this response.
	 *
	 * @var integer
	 */
	protected $statusCode = 200;

	/**
	 * Constructor.
	 *
	 * @param string  $filename
	 * @param boolean $setMime
	 */
	public function __construct(string $filename, bool $setMime)
	{
		parent::__construct(config('App'));

		$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.
	 *
	 * @param string $binary
	 */
	public function setBinary(string $binary)
	{
		if ($this->file !== null)
		{
			throw DownloadException::forCannotSetBinary();
		}

		$this->binary = $binary;
	}

	/**
	 * set download for file.
	 *
	 * @param string $filepath
	 */
	public function setFilePath(string $filepath)
	{
		if ($this->binary !== null)
		{
			throw DownloadException::forCannotSetFilePath($filepath);
		}

		$this->file = new File($filepath, true);
	}

	/**
	 * set name for the download.
	 *
	 * @param string $filename
	 *
	 * @return $this
	 */
	public function setFileName(string $filename)
	{
		$this->filename = $filename;
		return $this;
	}

	/**
	 * get content length.
	 *
	 * @return integer
	 */
	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()
	{
		$mime    = null;
		$charset = '';

		if ($this->setMime === true)
		{
			if (($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.
	 *
	 * @return string
	 */
	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.
	 *
	 * @return 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.
	 *
	 * @param integer $code
	 * @param string  $reason
	 *
	 * @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.
	 *
	 * @param string $mime
	 * @param string $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.
	 *
	 * @param array $options
	 *
	 * @throws DownloadException
	 */
	public function setCache(array $options = [])
	{
		throw DownloadException::forCannotSetCache();
	}

	//--------------------------------------------------------------------
	// Output Methods
	//--------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 *
	 * @todo Do downloads need CSP or Cookies? Compare with ResponseTrait::send()
	 */
	public function send()
	{
		$this->buildHeaders();
		$this->sendHeaders();
		$this->sendBody();

		return $this;
	}

	/**
	 * set header for file download.
	 */
	public function buildHeaders()
	{
		if (! $this->hasHeader('Content-Type'))
		{
			$this->setContentTypeByMimeType();
		}

		$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.
	 *
	 * @throws DownloadException
	 *
	 * @return DownloadResponse
	 */
	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(1048576)) !== false)
		{
			echo $data;
		}

		return $this;
	}

	/**
	 * output download text by binary
	 *
	 * @return DownloadResponse
	 */
	private function sendBodyByBinary()
	{
		echo $this->binary;

		return $this;
	}
}