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