ice framework documentation ice doc v1.10.1
Class Ice Http

Response

    
namespace Ice\Http;

use Ice\Di;
use Ice\Exception;
use Ice\Http\Response\Headers;
use Ice\Http\Request\RequestInterface;
use Ice\Http\Response\ResponseInterface;

/**
 * This class provides a simple interface around the HTTP response.
 *
 * @package     Ice/Http
 * @category    Component
 * @author      Ice Team
 * @copyright   (c) 2014-2023 Ice Team
 * @license     http://iceframework.org/license
 */
class Response implements ResponseInterface
{
    protected protocolVersion = "HTTP/1.1" { get, set };
    protected status = 200 { get, set };
    protected headers;
    protected loops = 16 { get, set };
    protected redirects = 0 { get };
    protected body { get, set };

    /**
     * Response constructor. Fetch Di and set it as a property.
     *
     * @param string|null body The HTTP response body
     * @param int status The HTTP response status
     */
    public function __construct(var body = null, int status = 200)
    {
        let this->headers = new Headers(),
            this->status = status,
            this->body = body;

        this->headers->set("Content-Type", "text/html");
    }

    /**
     * Get HTTP headers.
     *
     * @return array
     */
    public function getHeaders() -> array
    {
        return this->headers->all();
    }

    /**
     * Check whether request have a given header.
     *
     * @param string name
     * @return boolean
     */
    public function hasHeader(string name) -> boolean
    {
        return this->headers->has(name);
    }

    /**
     * Get header value.
     *
     * @param string name
     * @return string
     */
    public function getHeader(string name) -> string
    {
        return this->headers->get(name);
    }

    /**
     * Set header value.
     *
     * @param string name
     * @param string value
     * @return object Response
     */
    public function setHeader(string name, string value)
    {
        this->headers->set(name, value);

        return this;
    }

    /**
     * Set multiple header values.
     *
     * @param array headers
     * @return object Response
     */
    public function setHeaders(array headers)
    {
        this->headers->merge(headers);

        return this;
    }

    /**
     * Remove header by index name.
     *
     * @param string name
     * @return object Response
     */
    public function removeHeader(string name)
    {
        this->headers->remove(name);

        return this;
    }

    /**
     * Set body content.
     *
     * @param string content
     * @return object Response
     */
    public function setContent(string contet) -> object
    {
        let this->body = contet;

        return this;
    }

    /**
     * Finalize response for delivery to client.
     * Apply final preparations to the resposne object so that it is suitable for delivery to the client.
     *
     * @param RequestInterface request
     * @return object Response
     */
    public function finalize( request)
    {
        var sendBody;

        let sendBody = true;

        if in_array(this->status, [204, 304]) {
            this->headers->remove("Content-Type");
            this->headers->remove("Content-Length");
            let sendBody = false;
        }

        // Remove body if HEAD request
        if request->isHead() {
            let sendBody = false;
        }

        // Truncate body if it should not be sent with response
        if !sendBody {
            let this->body = "";
        }

        return this;
    }

    /**
     * Send HTTP response headers.
     *
     * @return object Response
     */
    public function send()
    {
        if !headers_sent() {
            if strpos(PHP_SAPI, "cgi") === 0 {
                header(sprintf("Status: %d %s", this->status, this->getMessage(this->status)));
            } else {
                header(sprintf("%s %d %s", this->getProtocolVersion(), this->status, this->getMessage(this->status)));
            }

            var di = Di::$fetch();

            if di->has("session") && !this->isRedirect() {
                di->get("session")->remove("_redirects");
            }

            this->headers->send();
        }

        return this;
    }

    /**
     * Send file download as the response. All execution will be halted when
     * this method is called! The third parameter allows the following
     * options to be set:
     *
     * Type      | Option    | Description                        | Default Value
     * ----------|-----------|------------------------------------|--------------
     * string    | file      | file that already exists           | null
     * boolean   | inline    | Display inline instead of download | FALSE
     * boolean   | resumable | Allow to resumable download        | FALSE
     * boolean   | delete    | Delete the file after sending      | FALSE
     * int       | timeout   | Execute time for the script        | 0
     * int       | speed     | Download speed in millisecond      | 0
     *
     * Download a file that already exists:
     *
     *     $request->sendFile('ice.zip', 'application/zip', ['file' => '/download/latest.zip']);
     *
     * Download generated content as a file:
     *
     *     $response->setContent($content);
     *     $response->sendFile($filename, $mineType);
     *
     * Attention: No further processing can be done after this method is called!
     *
     * @param string filename The file name of the attachment
     * @param string mime Manual mime type
     * @param array options The keys can be [file|inline|resumable|delete|timeout|speed]
     * @return  void
     */
    public function sendFile(string filename, string mime, array options = [])
    {
        var file, filepath, data, size, isDelete, disposition, block, pos, speed, range, start, end;

        if empty options["file"] {
            // Force the data to be rendered if
            let data = (string) this->body;

            // Get the content size
            let size = strlen(data);

            // Create a temporary file to hold the current response
            let file = tmpfile();

            // Write the current response into the file
            fwrite(file, data);

            let isDelete = false;
        } else {
            // Get the complete file path
            let filepath = realpath(options["file"]);

            // Get the file size
            let size = filesize(filepath);

            // Open the file for reading
            let file = fopen(filepath, "rb");

            fetch isDelete, options["delete"];
        }

        if !is_resource(file) {
            throw new Exception(["Could not read file to send: %s",  filename]);
        }

        if empty options["inline"] {
            let disposition = "attachment";
        } else {
            let disposition = "inline";
        }

        if empty options["resumable"] {
            this->headers->set("Content-Length", size);
        } else {
            // Calculate byte range to download.
            let range = this->getByteRange(size),
                start = range[0],
                end = range[1];

            // HTTP/1.1 416 Requested Range Not Satisfiable
            if this->status == 416 {
                header(sprintf("%s %d %s", this->getProtocolVersion(), this->status, this->getMessage(this->status)));
                exit();
            }

            // Partial Content
            if start > 0 || end < size - 1 {
                let this->status = 206;
            }

            // Range of bytes being sent
            this->headers->set("Accept-Ranges", "bytes");
            this->headers->set("Content-Range", "bytes ".start."-".end."/".size);
            this->headers->set("Content-Length", end - start + 1);
        }

        // Set the headers for a download
        this->headers->set("Content-Disposition", disposition."; filename=\"".filename."\"");
        this->headers->set("Content-Type", mime);

        // Send all headers now
        this->headers->send();

        while ob_get_level() {
            // Flush all output buffers
            ob_end_flush();
        }

        // Manually stop execution
        ignore_user_abort(true);

        if empty options["timeout"] {
            // Keep the script running forever
            set_time_limit(0);
        } else {
            set_time_limit((int)options["timeout"]);
        }

        // Send data in 16kb blocks
        let block = 1024 * 16;

        if !empty options["sleep"] {
            let speed = ((int)options["sleep"]) * 1000;
        } else {
            let speed = 0;
        }

        fseek(file, start);

        while !feof(file)  {
            let pos = ftell(file);

            if pos > end || connection_aborted() {
                break;
            }

            if pos + block > end {
                // Don't read past the buffer.
                let block = end - pos + 1;
            }

            // Output a block of the file
            echo fread(file, block);

            // Send the data now
            flush();

            if speed > 0 {
                usleep(speed);
            }
        }

        // Close the file
        fclose(file);

        if isDelete {
            try {
                // Attempt to remove the file
                unlink(filepath);
            } catch \Exception {
                // TODO: Write log for this exception
                // Do NOT display the exception, it will corrupt the output!
            }
        }

        // Stop execution
        exit();
    }

    /**
     * Redirect to some location.
     * This method prepares the response object to return an HTTP Redirect response to the client.
     *
     * @param string location The redirect destination
     * @param int status The redirect HTTP status code
     * @return object Response
     */
    public function redirect(string location = null, int status = 302, boolean external = false)
    {
        var url, di = Di::$fetch();;
        int redirects;

        this->setStatus(status);

        if !external {
            let url = di->get("url"),
                location = url->rel(location);
        }

        if di->has("session") && this->loops {
            var session = di->get("session");

            let redirects = (int) session->get("_redirects", 0) + 1,
                this->redirects = redirects;

            session->set("_redirects", redirects);

            if this->redirects > this->loops {
                return this;
            }
        }

        this->headers->set("Location", location);

        return this;
    }

    /**
     * Check whether status is for Empty.
     *
     * @return boolean
     */
    public function isEmpty() -> boolean
    {
        return in_array(this->status, [201, 204, 304]);
    }

    /**
     * Check whether status is for Informational.
     *
     * @return boolean
     */
    public function isInformational() -> boolean
    {
        return this->status >= 100 && this->status < 200;
    }

    /**
     * Check whether status is for OK.
     *
     * @return boolean
     */
    public function isOk() -> boolean
    {
        return this->status === 200;
    }

    /**
     * Check whether status is for Successful.
     *
     * @return boolean
     */
    public function isSuccessful() -> boolean
    {
        return this->status >= 200 && this->status < 300;
    }

    /**
     * Check whether status is for Redirect.
     *
     * @return boolean
     */
    public function isRedirect() -> boolean
    {
        return in_array(this->status, [301, 302, 303, 307]);
    }

    /**
     * Check whether status is for Redirection.
     *
     * @return boolean
     */
    public function isRedirection() -> boolean
    {
        return this->status >= 300 && this->status < 400;
    }

    /**
     * Check whether status is for Forbidden.
     *
     * @return boolean
     */
    public function isForbidden() -> boolean
    {
        return this->status === 403;
    }

    /**
     * Check whether status is for Not Found.
     *
     * @return boolean
     */
    public function isNotFound() -> boolean
    {
        return this->status === 404;
    }

    /**
     * Check whether status is for Client error.
     *
     * @return boolean
     */
    public function isClientError() -> boolean
    {
        return this->status >= 400 && this->status < 500;
    }

    /**
     * Check whether status is for Server Error.
     *
     * @return boolean
     */
    public function isServerError() -> boolean
    {
        return this->status >= 500 && this->status < 600;
    }

    /**
     * Get message for Response codes.
     *
     * @param int code Status code
     * @return string
     */
    public function getMessage(int code = 200) -> string
    {
        var message;

        fetch message, this->getMessages()[code];

        return message ? message : "";
    }

    /**
     * Get all messages.
     *
     * @return array
     */
    public function getMessages() -> array
    {
        return [
            //Informational 1xx
            100: "Continue",
            101: "Switching Protocols",
            102: "Processing",
            //Successful 2xx
            200: "OK",
            201: "Created",
            202: "Accepted",
            203: "Non-Authoritative Information",
            204: "No Content",
            205: "Reset Content",
            206: "Partial Content",
            207: "Multi-Status",
            208: "Already Reported",
            226: "IM Used",
            //Redirection 3xx
            300: "Multiple Choices",
            301: "Moved Permanently",
            302: "Found",
            303: "See Other",
            304: "Not Modified",
            305: "Use Proxy",
            306: "(Unused)",
            307: "Temporary Redirect",
            308: "Permanent Redirect",
            310: "Too Many Redirects",
            //Client Error 4xx
            400: "Bad Request",
            401: "Unauthorized",
            402: "Payment Required",
            403: "Forbidden",
            404: "Not Found",
            405: "Method Not Allowed",
            406: "Not Acceptable",
            407: "Proxy Authentication Required",
            408: "Request Timeout",
            409: "Conflict",
            410: "Gone",
            411: "Length Required",
            412: "Precondition Failed",
            413: "Request Entity Too Large",
            414: "Request-URI Too Long",
            415: "Unsupported Media Type",
            416: "Requested Range Not Satisfiable",
            417: "Expectation Failed",
            418: "I'm a teapot",
            422: "Unprocessable Entity",
            423: "Locked",
            424: "Failed Dependency",
            426: "Upgrade Required",
            428: "Precondition Required",
            429: "Too Many Requests",
            431: "Request Header Fields Too Large",
            //Server Error 5xx
            500: "Internal Server Error",
            501: "Not Implemented",
            502: "Bad Gateway",
            503: "Service Unavailable",
            504: "Gateway Timeout",
            505: "HTTP Version Not Supported",
            506: "Variant Also Negotiates",
            507: "Insufficient Storage",
            508: "Loop Detected",
            510: "Not Extended",
            511: "Network Authentication Required"
        ];
    }

    /**
     * Response data to JSON string.
     *
     * @param mixed data Can be any type excepta resource
     * @param int option The options for json_encode
     * @return object Response
     */
    public function toJson(var data, var option = null)
    {
        this->headers->set("Content-Type", "application/json;charset=utf-8");

        let this->body = json_encode(data, option);

        return this;
    }

    /**
     * Response data to XML string.
     *
     * 

     *     $response->toXml(
     *         [['title' => 'hello world', 'desc' => 'dont panic']],
     *         ['root' => 'blogs', 'namespace' => 'http://example.com/xml/blog']
     *     );
     *
     *     // This will output the xml
     *     
     *     hello worlddont panic
     * 
* * @param mixed data Can be any type excepta resource * @param array options The options can be [root|charset|namespace] * @return object Response */ public function toXml(var data, var options = null) { var doc, ns, rootName, charset; let rootName = isset options["root"] ? options["root"] : "result", charset = isset options["charset"] ? options["charset"] : "utf-8", doc = this->xmlEncode(data, rootName), doc->preserveWhiteSpace = false, doc->formatOutput = true, doc->encoding = charset; if fetch ns, options["namespace"] { doc->createAttributeNS(ns, "xmlns"); } this->headers->set("Content-Type", "application/xml;charset=" . charset); let this->body = doc->saveXML(); return this; } /** * Convert data to XML string. * * @param mixed data Can be any type excepta resource * @param string root The root tag name * @param DOMElement domNode null, ONLY FOR INTERNAL USE * @return DOMDocument domDoc object */ public function xmlEncode(var data, string root = "root", <\DOMElement> domNode = null) { var domDoc, type, key, val, node; if domNode === null { let domDoc = new \DOMDocument, domNode = domDoc->createElement(root); domDoc->appendChild(domNode); this->xmlEncode(data, null, domNode); return domDoc; } let domDoc = domNode->ownerDocument, type = typeof data; if type == "array" { for key, val in data { if typeof key == "integer" { let node = domDoc->createElement(rtrim(domNode->tagName, "s")); node->setAttribute("i", key); } else { let node = domDoc->createElement(key); } domNode->appendChild(node); this->xmlEncode(val, null, node); } } elseif type == "object" { // set internal attr __is__ eq object domNode->setAttribute("__is__", "obj"); for key, val in get_object_vars(data) { if typeof val == "array" || typeof val == "object" { let node = domDoc->createElement(key); domNode->appendChild(node); this->xmlEncode(val, null, node); } else { domNode->setAttribute(key, val); } } } else { if type == "boolean" { let data = data ? "true" : "false"; } domNode->appendChild(domDoc->createTextNode(data)); } } /** * Magic toString, convert response to string. * * @return string */ public function __toString() -> string { return (string) this->body; } /** * * At the moment we only support single ranges. * Multiple ranges requires some more work to ensure it works correctly * and comply with the spesifications: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 * * Multirange support annouces itself with: * header('Accept-Ranges: bytes'); * * Multirange content must be sent with multipart/byteranges mediatype, * as well as a boundry header to indicate the various chunks of data. * * @return array The bytes range start and end, error message if there is */ protected function getByteRange(int size) -> array { var start, end, range; let start = 0, end = size - 1; // Defaults to start with when the HTTP_RANGE header doesn't exist. if isset _SERVER["HTTP_RANGE"] { let range = explode("=", _SERVER["HTTP_RANGE"], 2); if range[0] != "bytes" { let this->status = 416; } else { // multiple ranges could be specified at the same time, // but for simplicity only serve the first range ATM let range = explode(",", range[1], 2), range = range[0]; // A negative value means we start from the end if range[0] == "-" { let start = abs(size - abs((int)range)); } else { let range = explode("-", range), start = abs((int)range[0]); if range[1] && is_numeric(range[1]) { let end = (int)range[1]; } } if end > size { let end = size - 1; } if start > end || start > size - 1 || end >= size { let this->status = 416; } } } return [start, end]; } }