Просмотр файла libarea-0.9/vendor/utopia-php/framework/src/Response.php

Размер файла: 20.48Kb
<?php

namespace Utopia;

class Response
{
    /**
     * HTTP content types
     */
    public const CONTENT_TYPE_TEXT = 'text/plain';

    public const CONTENT_TYPE_HTML = 'text/html';

    public const CONTENT_TYPE_JSON = 'application/json';

    public const CONTENT_TYPE_XML = 'text/xml';

    public const CONTENT_TYPE_JAVASCRIPT = 'text/javascript';

    public const CONTENT_TYPE_IMAGE = 'image/*';

    public const CONTENT_TYPE_IMAGE_JPEG = 'image/jpeg';

    public const CONTENT_TYPE_IMAGE_PNG = 'image/png';

    public const CONTENT_TYPE_IMAGE_GIF = 'image/gif';

    public const CONTENT_TYPE_IMAGE_SVG = 'image/svg+xml';

    public const CONTENT_TYPE_IMAGE_WEBP = 'image/webp';

    public const CONTENT_TYPE_IMAGE_ICON = 'image/x-icon';

    public const CONTENT_TYPE_IMAGE_BMP = 'image/bmp';

    /**
     * Chrsets
     */
    public const CHARSET_UTF8 = 'UTF-8';

    /**
     * HTTP response status codes
     */
    public const STATUS_CODE_CONTINUE = 100;

    public const STATUS_CODE_SWITCHING_PROTOCOLS = 101;

    public const STATUS_CODE_OK = 200;

    public const STATUS_CODE_CREATED = 201;

    public const STATUS_CODE_ACCEPTED = 202;

    public const STATUS_CODE_NON_AUTHORITATIVE_INFORMATION = 203;

    public const STATUS_CODE_NOCONTENT = 204;

    public const STATUS_CODE_RESETCONTENT = 205;

    public const STATUS_CODE_PARTIALCONTENT = 206;

    public const STATUS_CODE_MULTIPLE_CHOICES = 300;

    public const STATUS_CODE_MOVED_PERMANENTLY = 301;

    public const STATUS_CODE_FOUND = 302;

    public const STATUS_CODE_SEE_OTHER = 303;

    public const STATUS_CODE_NOT_MODIFIED = 304;

    public const STATUS_CODE_USE_PROXY = 305;

    public const STATUS_CODE_UNUSED = 306;

    public const STATUS_CODE_TEMPORARY_REDIRECT = 307;

    public const STATUS_CODE_BAD_REQUEST = 400;

    public const STATUS_CODE_UNAUTHORIZED = 401;

    public const STATUS_CODE_PAYMENT_REQUIRED = 402;

    public const STATUS_CODE_FORBIDDEN = 403;

    public const STATUS_CODE_NOT_FOUND = 404;

    public const STATUS_CODE_METHOD_NOT_ALLOWED = 405;

    public const STATUS_CODE_NOT_ACCEPTABLE = 406;

    public const STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED = 407;

    public const STATUS_CODE_REQUEST_TIMEOUT = 408;

    public const STATUS_CODE_CONFLICT = 409;

    public const STATUS_CODE_GONE = 410;

    public const STATUS_CODE_LENGTH_REQUIRED = 411;

    public const STATUS_CODE_PRECONDITION_FAILED = 412;

    public const STATUS_CODE_REQUEST_ENTITY_TOO_LARGE = 413;

    public const STATUS_CODE_REQUEST_URI_TOO_LONG = 414;

    public const STATUS_CODE_UNSUPPORTED_MEDIA_TYPE = 415;

    public const STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE = 416;

    public const STATUS_CODE_EXPECTATION_FAILED = 417;

    public const STATUS_CODE_TOO_EARLY = 425;

    public const STATUS_CODE_TOO_MANY_REQUESTS = 429;

    public const STATUS_CODE_INTERNAL_SERVER_ERROR = 500;

    public const STATUS_CODE_NOT_IMPLEMENTED = 501;

    public const STATUS_CODE_BAD_GATEWAY = 502;

    public const STATUS_CODE_SERVICE_UNAVAILABLE = 503;

    public const STATUS_CODE_GATEWAY_TIMEOUT = 504;

    public const STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED = 505;

    /**
     * @var array
     */
    protected $statusCodes = [
        self::STATUS_CODE_CONTINUE => 'Continue',
        self::STATUS_CODE_SWITCHING_PROTOCOLS => 'Switching Protocols',
        self::STATUS_CODE_OK => 'OK',
        self::STATUS_CODE_CREATED => 'Created',
        self::STATUS_CODE_ACCEPTED => 'Accepted',
        self::STATUS_CODE_NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information',
        self::STATUS_CODE_NOCONTENT => 'No Content',
        self::STATUS_CODE_RESETCONTENT => 'Reset Content',
        self::STATUS_CODE_PARTIALCONTENT => 'Partial Content',
        self::STATUS_CODE_MULTIPLE_CHOICES => 'Multiple Choices',
        self::STATUS_CODE_MOVED_PERMANENTLY => 'Moved Permanently',
        self::STATUS_CODE_FOUND => 'Found',
        self::STATUS_CODE_SEE_OTHER => 'See Other',
        self::STATUS_CODE_NOT_MODIFIED => 'Not Modified',
        self::STATUS_CODE_USE_PROXY => 'Use Proxy',
        self::STATUS_CODE_UNUSED => '(Unused)',
        self::STATUS_CODE_TEMPORARY_REDIRECT => 'Temporary Redirect',
        self::STATUS_CODE_BAD_REQUEST => 'Bad Request',
        self::STATUS_CODE_UNAUTHORIZED => 'Unauthorized',
        self::STATUS_CODE_PAYMENT_REQUIRED => 'Payment Required',
        self::STATUS_CODE_FORBIDDEN => 'Forbidden',
        self::STATUS_CODE_NOT_FOUND => 'Not Found',
        self::STATUS_CODE_METHOD_NOT_ALLOWED => 'Method Not Allowed',
        self::STATUS_CODE_NOT_ACCEPTABLE => 'Not Acceptable',
        self::STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',
        self::STATUS_CODE_REQUEST_TIMEOUT => 'Request Timeout',
        self::STATUS_CODE_CONFLICT => 'Conflict',
        self::STATUS_CODE_GONE => 'Gone',
        self::STATUS_CODE_LENGTH_REQUIRED => 'Length Required',
        self::STATUS_CODE_PRECONDITION_FAILED => 'Precondition Failed',
        self::STATUS_CODE_REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large',
        self::STATUS_CODE_REQUEST_URI_TOO_LONG => 'Request-URI Too Long',
        self::STATUS_CODE_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',
        self::STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable',
        self::STATUS_CODE_EXPECTATION_FAILED => 'Expectation Failed',
        self::STATUS_CODE_TOO_EARLY => 'Too Early',
        self::STATUS_CODE_TOO_MANY_REQUESTS => 'Too Many Requests',
        self::STATUS_CODE_INTERNAL_SERVER_ERROR => 'Internal Server Error',
        self::STATUS_CODE_NOT_IMPLEMENTED => 'Not Implemented',
        self::STATUS_CODE_BAD_GATEWAY => 'Bad Gateway',
        self::STATUS_CODE_SERVICE_UNAVAILABLE => 'Service Unavailable',
        self::STATUS_CODE_GATEWAY_TIMEOUT => 'Gateway Timeout',
        self::STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported',
    ];

    /**
     * Mime Types with compression support
     *
     * @var array
     */
    protected $compressed = [
        'text/plain' => true,
        'text/css' => true,
        'text/javascript' => true,
        'application/javascript' => true,
        'text/html' => true,
        'text/html; charset=UTF-8' => true,
        'application/json' => true,
        'application/json; charset=UTF-8' => true,
        'image/svg+xml' => true,
        'application/xml+rss' => true,
    ];

    public const COOKIE_SAMESITE_NONE = 'None';

    public const COOKIE_SAMESITE_STRICT = 'Strict';

    public const COOKIE_SAMESITE_LAX = 'Lax';

    public const CHUNK_SIZE = 2000000; //2mb

    /**
     * @var int
     */
    protected int $statusCode = self::STATUS_CODE_OK;

    /**
     * @var string
     */
    protected string $contentType = '';

    /**
     * @var bool
     */
    protected bool $disablePayload = false;

    /**
     * @var bool
     */
    protected bool $sent = false;

    /**
     * @var array
     */
    protected array $headers = [];

    /**
     * @var array
     */
    protected array $cookies = [];

    /**
     * @var float
     */
    protected float $startTime = 0;

    /**
     * @var int
     */
    protected int $size = 0;

    /**
     * Response constructor.
     *
     * @param  float  $time response start time
     */
    public function __construct(float $time = 0)
    {
        $this->startTime = (!empty($time)) ? $time : \microtime(true);
    }

    /**
     * Set content type
     *
     * Set HTTP content type header.
     *
     * @param  string  $type
     * @param  string  $charset
     */
    public function setContentType(string $type, string $charset = ''): static
    {
        $this->contentType = $type.((!empty($charset) ? '; charset='.$charset : ''));

        return $this;
    }

    /**
     * Get content type
     *
     * Get HTTP content type header.
     *
     * @return string
     */
    public function getContentType(): string
    {
        return $this->contentType;
    }

    /**
     * Get if response was already sent
     *
     * @return bool
     */
    public function isSent(): bool
    {
        return $this->sent;
    }

    /**
     * Set status code
     *
     * Set HTTP response status code between available options. if status code is unknown an exception will be thrown
     *
     * @param  int  $code
     *
     * @throws Exception
     */
    public function setStatusCode(int $code = 200): static
    {
        if (!\array_key_exists($code, $this->statusCodes)) {
            throw new Exception('Unknown HTTP status code');
        }

        $this->statusCode = $code;

        return $this;
    }

    /**
     * Get status code
     *
     * Get HTTP response status code
     *
     * @return int
     **/
    public function getStatusCode(): int
    {
        return $this->statusCode;
    }

    /**
     * Get Response Size
     *
     * Return output response size in bytes
     *
     * @return int
     */
    public function getSize(): int
    {
        return $this->size;
    }

    /**
     * Don't allow payload on response output
     */
    public function disablePayload(): static
    {
        $this->disablePayload = true;

        return $this;
    }

    /**
     * Allow payload on response output
     */
    public function enablePayload(): static
    {
        $this->disablePayload = false;

        return $this;
    }

    /**
     * Add header
     *
     * Add an HTTP response header
     *
     * @param  string  $key
     * @param  string  $value
     */
    public function addHeader(string $key, string $value): static
    {
        $this->headers[$key] = $value;

        return $this;
    }

    /**
     * Remove header
     *
     * Remove HTTP response header
     *
     * @param  string  $key
     */
    public function removeHeader(string $key): static
    {
        if (isset($this->headers[$key])) {
            unset($this->headers[$key]);
        }

        return $this;
    }

    /**
     * Get Headers
     *
     * Return array of all response headers
     *
     * @return array
     */
    public function getHeaders(): array
    {
        return $this->headers;
    }

    /**
     * Add cookie
     *
     * Add an HTTP cookie to response header
     *
     * @param  string  $name
     * @param  string  $value
     * @param  int  $expire
     * @param  string  $path
     * @param  string  $domain
     * @param  bool  $secure
     * @param  bool  $httponly
     * @param  string  $sameSite
     */
    public function addCookie(string $name, string $value = null, int $expire = null, string $path = null, string $domain = null, bool $secure = null, bool $httponly = null, string $sameSite = null): static
    {
        $name = strtolower($name);

        $this->cookies[] = [
            'name' => $name,
            'value' => $value,
            'expire' => $expire,
            'path' => $path,
            'domain' => $domain,
            'secure' => $secure,
            'httponly' => $httponly,
            'samesite' => $sameSite,
        ];

        return $this;
    }

    /**
     * Remove cookie
     *
     * Remove HTTP response cookie
     *
     * @param  string  $name
     */
    public function removeCookie(string $name): static
    {
        $this->cookies = array_filter($this->cookies, function ($cookie) use ($name) {
            return $cookie['name'] !== $name;
        });

        return $this;
    }

    /**
     * Get Cookies
     *
     * Return array of all response cookies
     *
     * @return array
     */
    public function getCookies(): array
    {
        return $this->cookies;
    }

    /**
     * Output response
     *
     * Generate HTTP response output including the response header (+cookies) and body and prints them.
     *
     * @param  string  $body
     * @return void
     */
    public function send(string $body = ''): void
    {
        if ($this->sent) {
            return;
        }

        $this->sent = true;

        $this->addHeader('X-Debug-Speed', (string) (\microtime(true) - $this->startTime));

        $this
            ->appendCookies()
            ->appendHeaders();

        if (!$this->disablePayload) {
            $length = strlen($body);

            $this->size = $this->size + strlen(implode("\n", $this->headers)) + $length;

            if (array_key_exists(
                $this->contentType,
                $this->compressed
            ) && ($length <= self::CHUNK_SIZE)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb
                $this->end($body);
            } else {
                for ($i = 0; $i < ceil($length / self::CHUNK_SIZE); $i++) {
                    $this->write(substr($body, ($i * self::CHUNK_SIZE), min(self::CHUNK_SIZE, $length - ($i * self::CHUNK_SIZE))));
                }

                $this->end();
            }

            $this->disablePayload();
        } else {
            $this->end();
        }
    }

    /**
     * Write
     *
     * Send output
     *
     * @param  string  $content
     * @return void
     */
    protected function write(string $content): void
    {
        echo $content;
    }

    /**
     * End
     *
     * Send optional content and end
     *
     * @param  string  $content
     * @return void
     */
    protected function end(string $content = null): void
    {
        if (!is_null($content)) {
            echo $content;
        }
    }

    /**
     * Output response
     *
     * Generate HTTP response output including the response header (+cookies) and body and prints them.
     *
     * @param  string  $body
     * @param bool $end
     *
     * @return void
     */
    public function chunk(string $body = '', bool $end = false): void
    {
        if ($this->sent) {
            return;
        }

        if ($end) {
            $this->sent = true;
        }

        $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime));

        $this
            ->appendCookies()
            ->appendHeaders();

        if (!$this->disablePayload) {
            $this->write($body);
            if ($end) {
                $this->disablePayload();
                $this->end();
            }
        } else {
            $this->end();
        }
    }

    /**
     * Append headers
     *
     * Iterating over response headers to generate them using native PHP header function.
     * This method is also responsible for generating the response and content type headers.
     */
    protected function appendHeaders(): static
    {
        // Send status code header
        $this->sendStatus($this->statusCode);

        // Send content type header
        if (!empty($this->contentType)) {
            $this->addHeader('Content-Type', $this->contentType);
        }

        // Set application headers
        foreach ($this->headers as $key => $value) {
            $this->sendHeader($key, $value);
        }

        return $this;
    }

    /**
     * Send Status Code
     *
     * @param  int  $statusCode
     * @return void
     */
    protected function sendStatus(int $statusCode): void
    {
        http_response_code($statusCode);
    }

    /**
     * Send Header
     *
     * Output Header
     *
     * @param  string  $key
     * @param  string  $value
     * @return void
     */
    protected function sendHeader(string $key, string $value): void
    {
        \header($key.': '.$value);
    }

    /**
     * Send Cookie
     *
     * Output Cookie
     *
     * @param  string  $name
     * @param  string  $value
     * @param  array  $options
     * @return void
     */
    protected function sendCookie(string $name, string $value, array $options): void
    {
        // Use proper PHP keyword name
        $options['expires'] = $options['expire'];
        unset($options['expire']);

        // Set the cookie
        \setcookie($name, $value, $options);
    }

    /**
     * Append cookies
     *
     * Iterating over response cookies to generate them using native PHP cookie function.
     */
    protected function appendCookies(): static
    {
        foreach ($this->cookies as $cookie) {
            $this->sendCookie($cookie['name'], $cookie['value'], [
                'expire' => $cookie['expire'],
                'path' => $cookie['path'],
                'domain' => $cookie['domain'],
                'secure' => $cookie['secure'],
                'httponly' => $cookie['httponly'],
                'samesite' => $cookie['samesite'],
            ]);
        }

        return $this;
    }

    /**
     * Redirect
     *
     * This helper is for sending a 30* HTTP response.
     * After setting relevant HTTP headers for redirect response this helper stop application native flow what means the shutdown method will not be executed
     *
     * NOTICE: it seems webkit based browsers have problems redirecting link with 300 status codes.
     *
     * @see https://code.google.com/p/chromium/issues/detail?id=75540
     * @see https://bugs.webkit.org/show_bug.cgi?id=47425
     *
     * @param  string  $url complete absolute URI for redirection as required by the internet standard RFC 2616 (HTTP 1.1)
     * @param  int  $statusCode valid HTTP status code
     * @return void
     *
     * @throws Exception
     *
     * @see http://tools.ietf.org/html/rfc2616
     */
    public function redirect(string $url, int $statusCode = 301): void
    {
        if (300 == $statusCode) {
            \trigger_error('It seems webkit based browsers have problems redirecting link with 300 status codes!', E_USER_NOTICE);
        }

        $this
            ->addHeader('Location', $url)
            ->setStatusCode($statusCode)
            ->send('');
    }

    /**
     * HTML
     *
     * This helper is for sending an HTML HTTP response and sets relevant content type header ('text/html').
     *
     * @see http://en.wikipedia.org/wiki/JSON
     *
     * @param  string  $data
     * @return void
     */
    public function html(string $data): void
    {
        $this
            ->setContentType(self::CONTENT_TYPE_HTML, self::CHARSET_UTF8)
            ->send($data);
    }

    /**
     * Text
     *
     * This helper is for sending plain text HTTP response and sets relevant content type header ('text/plain').
     *
     * @see http://en.wikipedia.org/wiki/JSON
     *
     * @param  string  $data
     * @return void
     */
    public function text(string $data): void
    {
        $this
            ->setContentType(self::CONTENT_TYPE_TEXT, self::CHARSET_UTF8)
            ->send($data);
    }

    /**
     * JSON
     *
     * This helper is for sending JSON HTTP response.
     * It sets relevant content type header ('application/json') and convert a PHP array ($data) to valid JSON using native json_encode
     *
     * @see http://en.wikipedia.org/wiki/JSON
     *
     * @param  mixed  $data
     * @return void
     */
    public function json($data): void
    {
        if (!is_array($data) && !$data instanceof \stdClass) {
            throw new \Exception('Invalid JSON input var');
        }

        $this
            ->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8)
            ->send(\json_encode($data, JSON_UNESCAPED_UNICODE));
    }

    /**
     * JSON with padding
     *
     * This helper is for sending JSONP HTTP response.
     * It sets relevant content type header ('text/javascript') and convert a PHP array ($data) to valid JSON using native json_encode
     *
     * @see http://en.wikipedia.org/wiki/JSONP
     *
     * @param  string  $callback
     * @param  array  $data
     * @return void
     */
    public function jsonp(string $callback, array $data): void
    {
        $this
            ->setContentType(self::CONTENT_TYPE_JAVASCRIPT, self::CHARSET_UTF8)
            ->send('parent.'.$callback.'('.\json_encode($data).');');
    }

    /**
     * Iframe
     *
     * This helper is for sending iframe HTTP response.
     * It sets relevant content type header ('text/html') and convert a PHP array ($data) to valid JSON using native json_encode
     *
     * @param  string  $callback
     * @param  array  $data
     * @return void
     */
    public function iframe(string $callback, array $data): void
    {
        $this
            ->setContentType(self::CONTENT_TYPE_HTML, self::CHARSET_UTF8)
            ->send('<script type="text/javascript">window.parent.'.$callback.'('.\json_encode($data).');</script>');
    }

    /**
     * No Content
     *
     * This helper is for sending no content HTTP response.
     *
     * The server has successfully fulfilled the request
     *  and that there is no additional content to send in the response payload body.
     *
     * @return void
     */
    public function noContent(): void
    {
        $this
            ->setStatusCode(self::STATUS_CODE_NOCONTENT)
            ->send('');
    }
}