<?php
/**
 * @link https://craftcms.com/
 * @copyright Copyright (c) Pixel & Tonic, Inc.
 * @license https://craftcms.github.io/license/
 */

namespace craft\web;

use Craft;
use yii\web\HttpException;

/**
 * @inheritdoc
 * @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
 * @since 3.0
 */
class Response extends \yii\web\Response
{
    // Properties
    // =========================================================================

    /**
     * @var bool whether the response has been prepared.
     */
    private $_isPrepared = false;

    // Public Methods
    // =========================================================================

    /**
     * Returns the Content-Type header (sans `charset=X`) that the response will most likely include.
     *
     * @return string|null
     */
    public function getContentType()
    {
        // If the response hasn't been prepared yet, go with what the formatter is going to set
        if (!$this->_isPrepared) {
            switch ($this->format) {
                case self::FORMAT_HTML:
                    return 'text/html';
                case self::FORMAT_XML:
                    return 'application/xml';
                case self::FORMAT_JSON:
                    return 'application/json';
                case self::FORMAT_JSONP:
                    return 'application/javascript';
            }
        }

        // Otherwise check the Content-Type header
        if (($header = $this->getHeaders()->get('content-type')) === null) {
            return null;
        }

        if (($pos = strpos($header, ';')) !== false) {
            $header = substr($header, 0, $pos);
        }

        return strtolower(trim($header));
    }

    /**
     * Sets headers that will instruct the client to cache this response.
     *
     * @return static self reference
     */
    public function setCacheHeaders()
    {
        $cacheTime = 31536000; // 1 year
        $this->getHeaders()
            ->set('Expires', gmdate('D, d M Y H:i:s', time() + $cacheTime) . ' GMT')
            ->set('Pragma', 'cache')
            ->set('Cache-Control', 'max-age=' . $cacheTime);

        return $this;
    }

    /**
     * Sets a Last-Modified header based on a given file path.
     *
     * @param string $path The file to read the last modified date from.
     * @return static self reference
     */
    public function setLastModifiedHeader(string $path)
    {
        $modifiedTime = filemtime($path);

        if ($modifiedTime) {
            $this->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $modifiedTime) . ' GMT');
        }

        return $this;
    }

    /**
     * @inheritdoc \yii\web\Response::sendFile()
     * @param string $filePath
     * @param string|null $attachmentName
     * @param array $options
     * @return static self reference
     */
    public function sendFile($filePath, $attachmentName = null, $options = [])
    {
        $this->_clearOutputBuffer();
        parent::sendFile($filePath, $attachmentName, $options);

        return $this;
    }

    /**
     * @inheritdoc \yii\web\Response::sendContentAsFile()
     * @param string $content
     * @param string $attachmentName
     * @param array $options
     * @return static self reference
     * @throws HttpException
     */
    public function sendContentAsFile($content, $attachmentName, $options = [])
    {
        $this->_clearOutputBuffer();
        parent::sendContentAsFile($content, $attachmentName, $options);

        return $this;
    }

    /**
     * Attempts to closes the connection with the HTTP client, without ending PHP script execution.
     *
     * This method relies on [flush()](http://php.net/manual/en/function.flush.php), which may not actually work if
     * mod_deflate or mod_gzip is installed, or if this is a Win32 server.
     *
     * @see http://stackoverflow.com/a/141026
     * @throws \Throwable An exception will be thrown if content has already been output.
     */
    public function sendAndClose()
    {
        // Make sure nothing has been output yet
        if (headers_sent()) {
            return;
        }

        // Prevent the script from ending when the browser closes the connection
        ignore_user_abort(true);

        // Prepend any current OB content
        while (ob_get_length() !== false) {
            // If ob_start() didn't have the PHP_OUTPUT_HANDLER_CLEANABLE flag, ob_get_clean() will cause a PHP notice
            // and return false.
            $obContent = @ob_get_clean();

            if ($obContent !== false) {
                $this->content = $obContent . $this->content;
            } else {
                break;
            }
        }

        // Tell the browser to close the connection
        $length = $this->content !== null ? strlen($this->content) : 0;
        $this->getHeaders()
            ->set('Connection', 'close')
            ->set('Content-Length', $length);

        $this->send();

        // Close the session.
        Craft::$app->getSession()->close();

        // In case we're running on php-fpm (https://secure.php.net/manual/en/book.fpm.php)
        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request();
        }
    }

    /**
     * @inheritdoc
     * @internal this is an exact copy of yii\web\Response::sendContent(), except for the `@` before `set_time_limit(0)`
     * @todo remove this if Yii ever merges https://github.com/yiisoft/yii2/pull/15679 or similar
     */
    protected function sendContent()
    {
        if ($this->stream === null) {
            echo $this->content;

            return;
        }

        @set_time_limit(0); // Reset time limit for big files
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk

        if (is_array($this->stream)) {
            list($handle, $begin, $end) = $this->stream;
            fseek($handle, $begin);
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
                if ($pos + $chunkSize > $end) {
                    $chunkSize = $end - $pos + 1;
                }
                echo fread($handle, $chunkSize);
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
            }
            fclose($handle);
        } else {
            while (!feof($this->stream)) {
                echo fread($this->stream, $chunkSize);
                flush();
            }
            fclose($this->stream);
        }
    }

    // Protected Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    protected function prepare()
    {
        $return = parent::prepare();
        $this->_isPrepared = true;

        return $return;
    }

    // Private Methods
    // =========================================================================

    /**
     * Clear the output buffer to prevent corrupt downloads.
     *
     * Need to check the OB status first, or else some PHP versions will throw an E_NOTICE
     * since we have a custom error handler (http://pear.php.net/bugs/bug.php?id=9670).
     */
    private function _clearOutputBuffer()
    {
        if (ob_get_length() !== false) {
            // If zlib.output_compression is enabled, then ob_clean() will corrupt the results of output buffering.
            // ob_end_clean is what we want.
            ob_end_clean();
        }
    }
}
