1: <?php
2: /**
3: * CHttpCacheFilter class file.
4: *
5: * @author Da:Sourcerer <webmaster@dasourcerer.net>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CHttpCacheFilter implements http caching. It works a lot like {@link COutputCache}
13: * as a filter, except that content caching is being done on the client side.
14: *
15: * @author Da:Sourcerer <webmaster@dasourcerer.net>
16: * @package system.web.filters
17: * @since 1.1.11
18: */
19: class CHttpCacheFilter extends CFilter
20: {
21: /**
22: * @var string|integer Timestamp for the last modification date.
23: * Must be either a string parsable by {@link http://php.net/strtotime strtotime()}
24: * or an integer representing a unix timestamp.
25: */
26: public $lastModified;
27: /**
28: * @var string|callback PHP Expression for the last modification date.
29: * If set, this takes precedence over {@link lastModified}.
30: *
31: * The PHP expression will be evaluated using {@link evaluateExpression}.
32: *
33: * A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
34: * please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
35: */
36: public $lastModifiedExpression;
37: /**
38: * @var mixed Seed for the ETag.
39: * Can be anything that passes through {@link http://php.net/serialize serialize()}.
40: */
41: public $etagSeed;
42: /**
43: * @var string|callback Expression for the ETag seed.
44: * If set, this takes precedence over {@link etagSeed}.
45: *
46: * The PHP expression will be evaluated using {@link evaluateExpression}.
47: *
48: * A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
49: * please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
50: */
51: public $etagSeedExpression;
52: /**
53: * @var string Http cache control headers. Set this to an empty string in order to keep this
54: * header from being sent entirely.
55: */
56: public $cacheControl='max-age=3600, public';
57:
58: /**
59: * Performs the pre-action filtering.
60: * @param CFilterChain $filterChain the filter chain that the filter is on.
61: * @return boolean whether the filtering process should continue and the action should be executed.
62: */
63: public function preFilter($filterChain)
64: {
65: // Only cache GET and HEAD requests
66: if(!in_array(Yii::app()->getRequest()->getRequestType(), array('GET', 'HEAD')))
67: return true;
68:
69: $lastModified=$this->getLastModifiedValue();
70: $etag=$this->getEtagValue();
71:
72: if($etag===false&&$lastModified===false)
73: return true;
74:
75: if($etag)
76: header('ETag: '.$etag);
77:
78: if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&isset($_SERVER['HTTP_IF_NONE_MATCH']))
79: {
80: if($this->checkLastModified($lastModified)&&$this->checkEtag($etag))
81: {
82: $this->send304Header();
83: $this->sendCacheControlHeader();
84: return false;
85: }
86: }
87: elseif(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
88: {
89: if($this->checkLastModified($lastModified))
90: {
91: $this->send304Header();
92: $this->sendCacheControlHeader();
93: return false;
94: }
95: }
96: elseif(isset($_SERVER['HTTP_IF_NONE_MATCH']))
97: {
98: if($this->checkEtag($etag))
99: {
100: $this->send304Header();
101: $this->sendCacheControlHeader();
102: return false;
103: }
104:
105: }
106:
107: if($lastModified)
108: header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');
109:
110: $this->sendCacheControlHeader();
111: return true;
112: }
113:
114: /**
115: * Gets the last modified value from either {@link lastModifiedExpression} or {@link lastModified}
116: * and converts it into a unix timestamp if necessary
117: * @throws CException
118: * @return integer|boolean A unix timestamp or false if neither lastModified nor
119: * lastModifiedExpression have been set
120: */
121: protected function getLastModifiedValue()
122: {
123: if($this->lastModifiedExpression)
124: {
125: $value=$this->evaluateExpression($this->lastModifiedExpression);
126: if(is_numeric($value)&&$value==(int)$value)
127: return $value;
128: elseif(($lastModified=strtotime($value))===false)
129: throw new CException(Yii::t('yii','Invalid expression for CHttpCacheFilter.lastModifiedExpression: The evaluation result "{value}" could not be understood by strtotime()',
130: array('{value}'=>$value)));
131: return $lastModified;
132: }
133:
134: if($this->lastModified)
135: {
136: if(is_numeric($this->lastModified)&&$this->lastModified==(int)$this->lastModified)
137: return $this->lastModified;
138: elseif(($lastModified=strtotime($this->lastModified))===false)
139: throw new CException(Yii::t('yii','CHttpCacheFilter.lastModified contained a value that could not be understood by strtotime()'));
140: return $lastModified;
141: }
142: return false;
143: }
144:
145: /**
146: * Gets the ETag out of either {@link etagSeedExpression} or {@link etagSeed}
147: * @return string|boolean Either a quoted string serving as ETag or false if neither etagSeed nor etagSeedExpression have been set
148: */
149: protected function getEtagValue()
150: {
151: if($this->etagSeedExpression)
152: return $this->generateEtag($this->evaluateExpression($this->etagSeedExpression));
153: elseif($this->etagSeed)
154: return $this->generateEtag($this->etagSeed);
155: return false;
156: }
157:
158: /**
159: * Check if the etag supplied by the client matches our generated one
160: * @param string $etag the supplied etag
161: * @return boolean true if the supplied etag matches $etag
162: */
163: protected function checkEtag($etag)
164: {
165: return isset($_SERVER['HTTP_IF_NONE_MATCH'])&&$_SERVER['HTTP_IF_NONE_MATCH']==$etag;
166: }
167:
168: /**
169: * Checks if the last modified date supplied by the client is still up to date
170: * @param integer $lastModified the last modified date
171: * @return boolean true if the last modified date sent by the client is newer or equal to $lastModified
172: */
173: protected function checkLastModified($lastModified)
174: {
175: return isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$lastModified;
176: }
177:
178: /**
179: * Sends the 304 HTTP status code to the client
180: */
181: protected function send304Header()
182: {
183: $httpVersion=Yii::app()->request->getHttpVersion();
184: header("HTTP/$httpVersion 304 Not Modified");
185: }
186:
187: /**
188: * Sends the cache control header to the client
189: * @see cacheControl
190: * @since 1.1.12
191: */
192: protected function sendCacheControlHeader()
193: {
194: if(Yii::app()->session->isStarted)
195: {
196: session_cache_limiter('public');
197: header('Pragma:',true);
198: }
199: header('Cache-Control: '.$this->cacheControl,true);
200: }
201:
202: /**
203: * Generates a quoted string out of the seed
204: * @param mixed $seed Seed for the ETag
205: */
206: protected function generateEtag($seed)
207: {
208: return '"'.base64_encode(sha1(serialize($seed),true)).'"';
209: }
210: }
211: