<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2008 - 2013 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flResponse Class
 *
 * Class to provide responses to requests.
 *
 * @package fusionLib
 */
class flResponse {

	/**
	 * The status code to return.
	 *
	 * @var int
	 */
	private $statusCode = 200;

	/**
	 * HTTP headers.
	 *
	 * @var array
	 */
	private $headers = array();

	/**
	 * The content type.
	 *
	 * @var string
	 */
	private $contentType = 'text/html';

	/**
	 * The character encoding.
	 *
	 * @var string
	 */
	private $charset = 'utf-8';

	/**
	 * Flags if the content can be cached.
	 *
	 * @var bool
	 */
	private $noCache = false;

	/**
	 * Flags if to use gzip compression of output.
	 *
	 * @var bool
	 */
	private $compressOutput;

	/**
	 * List of cookies to set.
	 *
	 * @var array
	 */
	private $cookies = array();

	/**
	 * The body to output.
	 *
	 * @var string
	 */
	private $body = '';

	/**
	 * Construct the response object.
	 */
	function __construct() {
		$this->compressOutput(true);
	}

	/**
	 * Flags if output should be compressed when sending to the browser.
	 *
	 * @param bool $enable true to compress output.
	 * @return flResponse This object.
	 */
	function compressOutput($enable) {
		$this->compressOutput = $enable && !ini_get('zlib.output_compression') && extension_loaded('zlib');
		return $this;
	}

	/**
	 * Set the status code to return.
	 *
	 * @param int $code The HTTP status code to return.
	 * @return flResponse This object.
	 */
	function setStatusCode($code) {
		$this->statusCode = $code;
		return $this;
	}

	/**
	 * Set a HTTP header.
	 *
	 * @param string $name The name of the header to set.
	 * @param string $value The value to set for the header.
	 * @param bool $replace true to replace the previous value if any.
	 * @return flResponse This object.
	 */
	function setHeader($name, $value, $replace = true) {
		if($replace || !isset($this->headers[$name]))
			$this->headers[$name] = $value;
		return $this;
	}

	/**
	 * Clear the current headers.
	 */
	function clearHeaders() {
		$this->headers = array();
	}

	/**
	 * Set the content type.
	 *
	 * @param string $type The content type or null to not send.
	 * @return flResponse This object.
	 */
	function setContentType($type) {
		$this->contentType = $type;
		return $this;
	}

	/**
	 * Set the charset.
	 *
	 * @param string $charset The charset or null to not send.
	 * @return flResponse This object.
	 */
	function setCharset($charset) {
		$this->charset = $charset;
		return $this;
	}

	/**
	 * Flag to send headers to disable caching.
	 *
	 * @param bool $cacheDisable true to disable caching or false to allow caching.
	 * @return flResponse This object.
	 */
	function noCache($cacheDisable = true) {
		$this->noCache = $cacheDisable;
		return $this;
	}

	/**
	 * Set a cookie.
	 *
	 * @param string $name The name of the cookie.
	 * @param string $value The value to set.
	 * @param int $expire The number of seconds until the cookie expires, 0 if the cookie
	 * should expire at the end of the session, negative value to delete cookie.
	 * @param string $domain The domain to set the cookie on.
	 * @param string $path The path for the cookie.
	 * @param bool $secure Flags if the cookie should be available over https only.
	 * @param bool $httpOnly Flags if the cookie is only available via http protocol, true.
	 * @return flResponse This object.
	 */
	function setCookie($name, $value, $expire = 0, $domain = '', $path = '/', $secure = false, $httpOnly = false) {
		$this->cookies[$name] = array('value' => $value,
									'expire' => $expire == 0 ? 0 : time() + $expire,
									'domain' => $domain,
									'path' => $path,
									'secure' => $secure,
									'httpOnly' => $httpOnly);
		return $this;
	}

	/**
	 * Issue a redirect header to the browser, this function never returns.
	 *
	 * @param string $uri The absolute URI to redirect to.
	 * @param bool $permanent Flags if to issue a temporary, 302, move (false) or a permanent 301 move (true).
	 */
	function redirectTo($uri, $permanent = false) {
		while(@ob_end_clean());
		$this->clearHeaders();
		$this->setStatusCode($permanent ? 301 : 302);
		$this->setHeader('Location', $uri);
		$this->sendHeaders();
		die;
	}

	/**
	 * Set the last modified date and time header.
	 *
	 * If the request contains an If-Modified-Since header with a matching time stamp then
	 * 304 Not Modified is returned and the application exits.
	 *
	 * @see http://www.zend.com/zend/spotlight/dynamic-pages.php?article=dynamic-pages&id=990
	 *
	 * @param int $timestamp The Unix timestamp when the page was last modified.
	 * @return flResponse This object.
	 */
	function setLastModified($timestamp) {

		// Convert timestamp to date and time
		$dateTime = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
		$etag = '"' . md5($dateTime) . '"';

		// Queue headers
		$this->setHeader('Last-Modified', $dateTime);
		$this->setHeader('ETag', $etag);

		// If If-Modified-Since present and doesn't match or If-None-Match present and doesn't match do in full
		if((!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && !isset($_SERVER['HTTP_IF_NONE_MATCH'])) ||
		   (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $_SERVER['HTTP_IF_MODIFIED_SINCE'] != $dateTime) ||
		   (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] != $etag))
			return $this;

		// no change, dump output buffers, return 304 and stop the script
		while(@ob_end_clean());
		$this->setStatusCode(304);
		$this->sendHeaders();
		die;
	}

	/**
	 * Send headers to the client.
	 */
	function sendHeaders() {
		// Send the status code
		$statusCodes = array(
			100 => 'Continue',
			200 => 'OK',
			201 => 'Created',
			202 => 'Accepted',
			204 => 'No Content',
			205 => 'Reset Content',
			206 => 'Partial Content',
			300 => 'Multiple Choices',
			301 => 'Moved Permanently',
			302 => 'Found',
			303 => 'See Other',
			304 => 'Not Modified',
			307 => 'Temporary Redirect',
			400 => 'Bad Request',
			401 => 'Unauthorized',
			402 => 'Payment Required',
			403 => 'Forbidden',
			404 => 'Not Found',
			405 => 'Method Not Allowed',
			406 => 'Not Acceptable',
			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',
			500 => 'Internal Server Error',
			501 => 'Not Implemented',
			503 => 'Service Unavailable');
		header('Status: ' . $this->statusCode . ' ' . (isset($statusCodes[$this->statusCode]) ? $statusCodes[$this->statusCode] : '(unknown)'));
		header('HTTP/1.0 ' . $this->statusCode . ' ' . (isset($statusCodes[$this->statusCode]) ? $statusCodes[$this->statusCode] : '(unknown)'));

		// Send content type
		if($this->contentType !== null)
			header("Content-Type: $this->contentType" . ($this->charset === null ? '' : "; charset=$this->charset"));

		// If no caching
		if($this->noCache) {
			header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
			header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
			header('Cache-Control: no-store, no-cache, must-revalidate');
			header('Cache-Control: post-check=0, pre-check=0', false);
			header('Pragma: no-cache');
			header('ETag: ' . md5(time()));
		}

		// Send headers
		foreach($this->headers as $n => $v)
			header("$n: $v");

		// Send cookies
		foreach($this->cookies as $n => $v)
			setcookie($n, $v['value'], $v['expire'], $v['path'], $v['domain'], $v['secure'], $v['httpOnly']);

	}

	/**
	 * Set the body content.
	 *
	 * @param string $content The content to set.
	 * @return flResponse This object.
	 */
	function setBody($content) {
		$this->body = $content;
		return $this;
	}

	/**
	 * Send the response to the browser.
	 */
	function sendResponse() {
		$this->sendHeaders();

		while(@ob_end_clean());

		if($this->compressOutput)
			ob_start('ob_gzhandler');
		else
			header('Content-Length: ' . strlen($this->body));
		echo $this->body;
		if($this->compressOutput) ob_end_flush();
	}
}
