<?php
/**
 * Slim - a micro PHP 5 framework
 *
 * @author      Josh Lockhart <info@joshlockhart.com>
 * @copyright   2011 Josh Lockhart
 * @link        http://www.slimframework.com
 * @license     http://www.slimframework.com/license
 * @version     1.5.0
 *
 * MIT LICENSE
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * Response
 *
 * Object-oriented representation of an HTTP response that is
 * returned to the client. This class is responsible for:
 *
 * - HTTP response status
 * - HTTP response body
 * - HTTP response headers
 * - HTTP response cookies
 *
 * @package Slim
 * @author  Josh Lockhart <info@joshlockhart.com>
 * @author  Kris Jordan <http://github.com/KrisJordan>
 * @since   Version 1.0
 */
class Slim_Http_Response {

    /**
     * @var Slim_Http_Request
     */
    protected $request;

    /**
     * @var string
     */
    protected $httpVersion = '1.1';

    /**
     * @var int HTTP status code
     */
    protected $status = 200;

    /**
     * @var array Key-value array of HTTP response headers
     */
    protected $headers = array();

    /**
     * @var string HTTP response body
     */
    protected $body = '';

    /**
     * @var int Length of HTTP response body
     */
    protected $length = 0;

    /**
     * @var array HTTP response codes and messages
     */
    protected static $messages = array(
        //Informational 1xx
        100 => '100 Continue',
        101 => '101 Switching Protocols',
        //Successful 2xx
        200 => '200 OK',
        201 => '201 Created',
        202 => '202 Accepted',
        203 => '203 Non-Authoritative Information',
        204 => '204 No Content',
        205 => '205 Reset Content',
        206 => '206 Partial Content',
        //Redirection 3xx
        300 => '300 Multiple Choices',
        301 => '301 Moved Permanently',
        302 => '302 Found',
        303 => '303 See Other',
        304 => '304 Not Modified',
        305 => '305 Use Proxy',
        306 => '306 (Unused)',
        307 => '307 Temporary Redirect',
        //Client Error 4xx
        400 => '400 Bad Request',
        401 => '401 Unauthorized',
        402 => '402 Payment Required',
        403 => '403 Forbidden',
        404 => '404 Not Found',
        405 => '405 Method Not Allowed',
        406 => '406 Not Acceptable',
        407 => '407 Proxy Authentication Required',
        408 => '408 Request Timeout',
        409 => '409 Conflict',
        410 => '410 Gone',
        411 => '411 Length Required',
        412 => '412 Precondition Failed',
        413 => '413 Request Entity Too Large',
        414 => '414 Request-URI Too Long',
        415 => '415 Unsupported Media Type',
        416 => '416 Requested Range Not Satisfiable',
        417 => '417 Expectation Failed',
        422 => '422 Unprocessable Entity',
        423 => '423 Locked',
        //Server Error 5xx
        500 => '500 Internal Server Error',
        501 => '501 Not Implemented',
        502 => '502 Bad Gateway',
        503 => '503 Service Unavailable',
        504 => '504 Gateway Timeout',
        505 => '505 HTTP Version Not Supported'
    );

    /**
     * @var CookieJar Manages Cookies to be sent with this Response
     */
    protected $cookieJar;

    /**
     * Constructor
     */
    public function __construct( Slim_Http_Request $req ) {
        $this->request = $req;
        $this->header('Content-Type', 'text/html');
    }

    /**
     * Set and/or get the HTTP response version
     * @param   string $version
     * @return  void
     * @throws  InvalidArgumentException If argument is not a valid HTTP version
     */
    public function httpVersion( $version = null ) {
        if ( $version ) {
            $version = (string)$version;
            if ( $version === '1.0' || $version === '1.1' ) {
                $this->httpVersion = $version;
            } else {
                throw new InvalidArgumentException('Invalid HTTP version in Response object');
            }
        }
        return $this->httpVersion;
    }

    /**
     * Set and/or get the HTTP response status code
     * @param   int $status
     * @return  int
     * @throws  InvalidArgumentException If argument is not a valid HTTP status code
     */
    public function status( $status = null ) {
        if ( !is_null($status) ) {
            if ( !in_array(intval($status), array_keys(self::$messages)) ) {
                throw new InvalidArgumentException('Cannot set Response status. Provided status code "' . $status . '" is not a valid HTTP response code.');
            }
            $this->status = intval($status);
        }
        return $this->status;
    }

    /**
     * Get HTTP response headers
     * @return array
     */
    public function headers() {
        return $this->headers;
    }

    /**
     * Get and/or set an HTTP response header
     * @param   string      $key    The header name
     * @param   string      $value  The header value
     * @return  string|null         The header value, or NULL if header not set
     */
    public function header( $key, $value = null ) {
        if ( !is_null($value) ) {
            $this->headers[$key] = $value;
        }
        return isset($this->headers[$key]) ? $this->headers[$key] : null;
    }

    /**
     * Set the HTTP response body
     * @param   string $body    The new HTTP response body
     * @return  string          The new HTTP response body
     */
    public function body( $body = null ) {
        if ( !is_null($body) ) {
            $this->body = '';
            $this->length = 0;
            $this->write($body);
        }
        return $this->body;
    }

    /**
     * Append the HTTP response body
     * @param   string $body    Content to append to the current HTTP response body
     * @return  string          The updated HTTP response body
     */
    public function write( $body ) {
        $body = (string)$body;
        $this->length += strlen($body);
        $this->body .= $body;
        $this->header('Content-Length', $this->length);
        return $body;
    }

    /**
     * Set cookie jar
     * @param   Slim_Http_CookieJar $cookieJar
     * @return  void
     */
    public function setCookieJar( Slim_Http_CookieJar $cookieJar ) {
        $this->cookieJar = $cookieJar;
    }

    /**
     * Get cookie jar
     * @return Slim_Http_CookieJar
     */
    public function getCookieJar() {
        return $this->cookieJar;
    }

    /**
     * Finalize response headers before response is sent
     * @return void
     */
    public function finalize() {
        if ( in_array($this->status, array(204, 304)) ) {
            $this->body('');
            unset($this->headers['Content-Type']);
        }
    }

    /**
     * Get message for HTTP status code
     * @return string|null
     */
    public static function getMessageForCode( $status ) {
        return isset(self::$messages[$status]) ? self::$messages[$status] : null;
    }

    /**
     * Can this HTTP response have a body?
     * @return bool
     */
    public function canHaveBody() {
        return ( $this->status < 100 || $this->status >= 200 ) && $this->status != 204 && $this->status != 304;
    }

    /**
     * Send headers for HTTP response
     * @return void
     */
    protected function sendHeaders() {
        //Finalize response
        $this->finalize();

        if ( substr(PHP_SAPI, 0, 3) === 'cgi') {
            //Send Status header if running with fastcgi
            header('Status: ' . self::getMessageForCode($this->status()));
        } else {
            //Else send HTTP message
            header(sprintf('HTTP/%s %s', $this->httpVersion, self::getMessageForCode($this->status())));
        }

        //Send headers
        foreach ( $this->headers() as $name => $value ) {
            header("$name: $value");
        }

        //Send cookies
        foreach ( $this->getCookieJar()->getResponseCookies() as $name => $cookie ) {
            /* httponly option is only available for PHP version >= 5.2 */
            if ( version_compare(PHP_VERSION, '5.2.0', '>=') ) {
                setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpires(), $cookie->getPath(), $cookie->getDomain(), $cookie->getSecure(), $cookie->getHttpOnly());
            } else {
                setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpires(), $cookie->getPath(), $cookie->getDomain(), $cookie->getSecure());
            }
        }

        //Flush all output to client
        flush();
    }

    /**
     * Send HTTP response
     *
     * This method will set Response headers, set Response cookies,
     * and `echo` the Response body to the current output buffer.
     *
     * @return void
     */
    public function send() {
        if ( !headers_sent() ) {
            $this->sendHeaders();
        }
        if ( $this->canHaveBody() && $this->request->isHead() === false ) {
            echo $this->body;
        }
    }

}