1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Http\Header; 11 12/** 13 * @throws Exception\InvalidArgumentException 14 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 15 */ 16class CacheControl implements HeaderInterface 17{ 18 /** 19 * @var string 20 */ 21 protected $value; 22 23 /** 24 * Array of Cache-Control directives 25 * 26 * @var array 27 */ 28 protected $directives = array(); 29 30 /** 31 * Creates a CacheControl object from a headerLine 32 * 33 * @param string $headerLine 34 * @throws Exception\InvalidArgumentException 35 * @return CacheControl 36 */ 37 public static function fromString($headerLine) 38 { 39 list($name, $value) = GenericHeader::splitHeaderLine($headerLine); 40 41 // check to ensure proper header type for this factory 42 if (strtolower($name) !== 'cache-control') { 43 throw new Exception\InvalidArgumentException(sprintf( 44 'Invalid header line for Cache-Control string: ""', 45 $name 46 )); 47 } 48 49 HeaderValue::assertValid($value); 50 $directives = static::parseValue($value); 51 52 // @todo implementation details 53 $header = new static(); 54 foreach ($directives as $key => $value) { 55 $header->addDirective($key, $value); 56 } 57 58 return $header; 59 } 60 61 /** 62 * Required from HeaderDescription interface 63 * 64 * @return string 65 */ 66 public function getFieldName() 67 { 68 return 'Cache-Control'; 69 } 70 71 /** 72 * Checks if the internal directives array is empty 73 * 74 * @return bool 75 */ 76 public function isEmpty() 77 { 78 return empty($this->directives); 79 } 80 81 /** 82 * Add a directive 83 * For directives like 'max-age=60', $value = '60' 84 * For directives like 'private', use the default $value = true 85 * 86 * @param string $key 87 * @param string|bool $value 88 * @return CacheControl - provides the fluent interface 89 */ 90 public function addDirective($key, $value = true) 91 { 92 HeaderValue::assertValid($key); 93 if (! is_bool($value)) { 94 HeaderValue::assertValid($value); 95 } 96 $this->directives[$key] = $value; 97 return $this; 98 } 99 100 /** 101 * Check the internal directives array for a directive 102 * 103 * @param string $key 104 * @return bool 105 */ 106 public function hasDirective($key) 107 { 108 return array_key_exists($key, $this->directives); 109 } 110 111 /** 112 * Fetch the value of a directive from the internal directive array 113 * 114 * @param string $key 115 * @return string|null 116 */ 117 public function getDirective($key) 118 { 119 return array_key_exists($key, $this->directives) ? $this->directives[$key] : null; 120 } 121 122 /** 123 * Remove a directive 124 * 125 * @param string $key 126 * @return CacheControl - provides the fluent interface 127 */ 128 public function removeDirective($key) 129 { 130 unset($this->directives[$key]); 131 return $this; 132 } 133 134 /** 135 * Assembles the directives into a comma-delimited string 136 * 137 * @return string 138 */ 139 public function getFieldValue() 140 { 141 $parts = array(); 142 ksort($this->directives); 143 foreach ($this->directives as $key => $value) { 144 if (true === $value) { 145 $parts[] = $key; 146 } else { 147 if (preg_match('#[^a-zA-Z0-9._-]#', $value)) { 148 $value = '"' . $value.'"'; 149 } 150 $parts[] = "$key=$value"; 151 } 152 } 153 return implode(', ', $parts); 154 } 155 156 /** 157 * Returns a string representation of the HTTP Cache-Control header 158 * 159 * @return string 160 */ 161 public function toString() 162 { 163 return 'Cache-Control: ' . $this->getFieldValue(); 164 } 165 166 /** 167 * Internal function for parsing the value part of a 168 * HTTP Cache-Control header 169 * 170 * @param string $value 171 * @throws Exception\InvalidArgumentException 172 * @return array 173 */ 174 protected static function parseValue($value) 175 { 176 $value = trim($value); 177 178 $directives = array(); 179 180 // handle empty string early so we don't need a separate start state 181 if ($value == '') { 182 return $directives; 183 } 184 185 $lastMatch = null; 186 187 state_directive: 188 switch (static::match(array('[a-zA-Z][a-zA-Z_-]*'), $value, $lastMatch)) { 189 case 0: 190 $directive = $lastMatch; 191 goto state_value; 192 // intentional fall-through 193 194 default: 195 throw new Exception\InvalidArgumentException('expected DIRECTIVE'); 196 } 197 198 state_value: 199 switch (static::match(array('="[^"]*"', '=[^",\s;]*'), $value, $lastMatch)) { 200 case 0: 201 $directives[$directive] = substr($lastMatch, 2, -1); 202 goto state_separator; 203 // intentional fall-through 204 205 case 1: 206 $directives[$directive] = rtrim(substr($lastMatch, 1)); 207 goto state_separator; 208 // intentional fall-through 209 210 default: 211 $directives[$directive] = true; 212 goto state_separator; 213 } 214 215 state_separator: 216 switch (static::match(array('\s*,\s*', '$'), $value, $lastMatch)) { 217 case 0: 218 goto state_directive; 219 // intentional fall-through 220 221 case 1: 222 return $directives; 223 224 default: 225 throw new Exception\InvalidArgumentException('expected SEPARATOR or END'); 226 227 } 228 } 229 230 /** 231 * Internal function used by parseValue to match tokens 232 * 233 * @param array $tokens 234 * @param string $string 235 * @param string $lastMatch 236 * @return int 237 */ 238 protected static function match($tokens, &$string, &$lastMatch) 239 { 240 // Ensure we have a string 241 $value = (string) $string; 242 243 foreach ($tokens as $i => $token) { 244 if (preg_match('/^' . $token . '/', $value, $matches)) { 245 $lastMatch = $matches[0]; 246 $string = substr($value, strlen($matches[0])); 247 return $i; 248 } 249 } 250 return -1; 251 } 252} 253