<?php
/**
 * CHttpCacheFilter class file.
 *
 * @author Da:Sourcerer <webmaster@dasourcerer.net>
 * @link https://www.yiiframework.com/
 * @copyright 2008-2013 Yii Software LLC
 * @license https://www.yiiframework.com/license/
 */

/**
 * CHttpCacheFilter implements http caching. It works a lot like {@link COutputCache}
 * as a filter, except that content caching is being done on the client side.
 *
 * @author Da:Sourcerer <webmaster@dasourcerer.net>
 * @package system.web.filters
 * @since 1.1.11
 */
class CHttpCacheFilter extends CFilter
{
	/**
	 * @var string|integer Timestamp for the last modification date.
	 * Must be either a string parsable by {@link https://php.net/strtotime strtotime()}
	 * or an integer representing a unix timestamp.
	 */
	public $lastModified;
	/**
	 * @var string|callback PHP Expression for the last modification date.
	 * If set, this takes precedence over {@link lastModified}.
	 *
	 * The PHP expression will be evaluated using {@link evaluateExpression}.
	 *
	 * A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
	 * please refer to the {@link https://www.php.net/manual/en/language.expressions.php php manual}.
	 */
	public $lastModifiedExpression;
	/**
	 * @var mixed Seed for the ETag.
	 * Can be anything that passes through {@link https://php.net/serialize serialize()}.
	 */
	public $etagSeed;
	/**
	 * @var string|callback Expression for the ETag seed.
	 * If set, this takes precedence over {@link etagSeed}.
	 *
	 * The PHP expression will be evaluated using {@link evaluateExpression}.
	 *
	 * A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
	 * please refer to the {@link https://www.php.net/manual/en/language.expressions.php php manual}.
	 */
	public $etagSeedExpression;
	/**
	 * @var string Http cache control headers. Set this to an empty string in order to keep this
	 * header from being sent entirely.
	 */
	public $cacheControl='max-age=3600, public';

	/**
	 * Performs the pre-action filtering.
	 * @param CFilterChain $filterChain the filter chain that the filter is on.
	 * @return boolean whether the filtering process should continue and the action should be executed.
	 */
	public function preFilter($filterChain)
	{
		// Only cache GET and HEAD requests
		if(!in_array(Yii::app()->getRequest()->getRequestType(), array('GET', 'HEAD')))
			return true;

		$lastModified=$this->getLastModifiedValue();
		$etag=$this->getEtagValue();

		if($etag===false&&$lastModified===false)
			return true;

		if($etag)
			header('ETag: '.$etag);

		$this->sendCacheControlHeader();

		$cacheValid = false;
		if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&isset($_SERVER['HTTP_IF_NONE_MATCH']))
		{
			if($this->checkLastModified($lastModified)&&$this->checkEtag($etag))
			{
				$cacheValid=true;
			}
		}
		elseif(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
		{
			if($this->checkLastModified($lastModified))
			{
				$cacheValid=true;
			}
		}
		elseif(isset($_SERVER['HTTP_IF_NONE_MATCH']))
		{
			if($this->checkEtag($etag))
			{
				$cacheValid=true;
			}
		}

		if($lastModified)
			header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');

		if ($cacheValid) {
			$this->send304Header();
			return false;
		}

		return true;
	}

	/**
	 * Gets the last modified value from either {@link lastModifiedExpression} or {@link lastModified}
	 * and converts it into a unix timestamp if necessary
	 * @throws CException
	 * @return integer|boolean A unix timestamp or false if neither lastModified nor
	 * lastModifiedExpression have been set
	 */
	protected function getLastModifiedValue()
	{
		if($this->lastModifiedExpression)
		{
			$value=$this->evaluateExpression($this->lastModifiedExpression);
			if(is_numeric($value)&&$value==(int)$value)
				return $value;
			elseif(($lastModified=strtotime($value))===false)
				throw new CException(Yii::t('yii','Invalid expression for CHttpCacheFilter.lastModifiedExpression: The evaluation result "{value}" could not be understood by strtotime()',
					array('{value}'=>$value)));
			return $lastModified;
		}

		if($this->lastModified)
		{
			if(is_numeric($this->lastModified)&&$this->lastModified==(int)$this->lastModified)
				return $this->lastModified;
			elseif(($lastModified=strtotime($this->lastModified))===false)
				throw new CException(Yii::t('yii','CHttpCacheFilter.lastModified contained a value that could not be understood by strtotime()'));
			return $lastModified;
		}
		return false;
	}

	/**
	 *  Gets the ETag out of either {@link etagSeedExpression} or {@link etagSeed}
	 *  @return string|boolean Either a quoted string serving as ETag or false if neither etagSeed nor etagSeedExpression have been set
	 */
	protected function getEtagValue()
	{
		if($this->etagSeedExpression)
			return $this->generateEtag($this->evaluateExpression($this->etagSeedExpression));
		elseif($this->etagSeed)
			return $this->generateEtag($this->etagSeed);
		return false;
	}

	/**
	 * Check if the etag supplied by the client matches our generated one
	 * @param string $etag the supplied etag
	 * @return boolean true if the supplied etag matches $etag
	 */
	protected function checkEtag($etag)
	{
		return isset($_SERVER['HTTP_IF_NONE_MATCH'])&&$_SERVER['HTTP_IF_NONE_MATCH']==$etag;
	}

	/**
	 * Checks if the last modified date supplied by the client is still up to date
	 * @param integer $lastModified the last modified date
	 * @return boolean true if the last modified date sent by the client is newer or equal to $lastModified
	 */
	protected function checkLastModified($lastModified)
	{
		return isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$lastModified;
	}

	/**
	 * Sends the 304 HTTP status code to the client
	 */
	protected function send304Header()
	{
		$httpVersion=Yii::app()->request->getHttpVersion();
		header("HTTP/$httpVersion 304 Not Modified");
	}

	/**
	 * Sends the cache control header to the client
	 * @see cacheControl
	 * @since 1.1.12
	 */
	protected function sendCacheControlHeader()
	{
		if(Yii::app()->session->isStarted)
		{
			Yii::app()->session->setCacheLimiter('public');
			header('Pragma:',true);
		}
		header('Cache-Control: '.$this->cacheControl,true);
	}

	/**
	 * Generates a quoted string out of the seed
	 * @param mixed $seed Seed for the ETag
	 * @return string Quoted string serving as ETag
	 */
	protected function generateEtag($seed)
	{
		return '"'.base64_encode(sha1(serialize($seed),true)).'"';
	}
}
