1<?php
2namespace GuzzleHttp\Cookie;
3
4use GuzzleHttp\ToArrayInterface;
5
6/**
7 * Set-Cookie object
8 */
9class SetCookie implements ToArrayInterface
10{
11    /** @var array */
12    private static $defaults = [
13        'Name'     => null,
14        'Value'    => null,
15        'Domain'   => null,
16        'Path'     => '/',
17        'Max-Age'  => null,
18        'Expires'  => null,
19        'Secure'   => false,
20        'Discard'  => false,
21        'HttpOnly' => false
22    ];
23
24    /** @var array Cookie data */
25    private $data;
26
27    /**
28     * Create a new SetCookie object from a string
29     *
30     * @param string $cookie Set-Cookie header string
31     *
32     * @return self
33     */
34    public static function fromString($cookie)
35    {
36        // Create the default return array
37        $data = self::$defaults;
38        // Explode the cookie string using a series of semicolons
39        $pieces = array_filter(array_map('trim', explode(';', $cookie)));
40        // The name of the cookie (first kvp) must include an equal sign.
41        if (empty($pieces) || !strpos($pieces[0], '=')) {
42            return new self($data);
43        }
44
45        // Add the cookie pieces into the parsed data array
46        foreach ($pieces as $part) {
47
48            $cookieParts = explode('=', $part, 2);
49            $key = trim($cookieParts[0]);
50            $value = isset($cookieParts[1])
51                ? trim($cookieParts[1], " \n\r\t\0\x0B\"")
52                : true;
53
54            // Only check for non-cookies when cookies have been found
55            if (empty($data['Name'])) {
56                $data['Name'] = $key;
57                $data['Value'] = $value;
58            } else {
59                foreach (array_keys(self::$defaults) as $search) {
60                    if (!strcasecmp($search, $key)) {
61                        $data[$search] = $value;
62                        continue 2;
63                    }
64                }
65                $data[$key] = $value;
66            }
67        }
68
69        return new self($data);
70    }
71
72    /**
73     * @param array $data Array of cookie data provided by a Cookie parser
74     */
75    public function __construct(array $data = [])
76    {
77        $this->data = array_replace(self::$defaults, $data);
78        // Extract the Expires value and turn it into a UNIX timestamp if needed
79        if (!$this->getExpires() && $this->getMaxAge()) {
80            // Calculate the Expires date
81            $this->setExpires(time() + $this->getMaxAge());
82        } elseif ($this->getExpires() && !is_numeric($this->getExpires())) {
83            $this->setExpires($this->getExpires());
84        }
85    }
86
87    public function __toString()
88    {
89        $str = $this->data['Name'] . '=' . $this->data['Value'] . '; ';
90        foreach ($this->data as $k => $v) {
91            if ($k != 'Name' && $k != 'Value' && $v !== null && $v !== false) {
92                if ($k == 'Expires') {
93                    $str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; ';
94                } else {
95                    $str .= ($v === true ? $k : "{$k}={$v}") . '; ';
96                }
97            }
98        }
99
100        return rtrim($str, '; ');
101    }
102
103    public function toArray()
104    {
105        return $this->data;
106    }
107
108    /**
109     * Get the cookie name
110     *
111     * @return string
112     */
113    public function getName()
114    {
115        return $this->data['Name'];
116    }
117
118    /**
119     * Set the cookie name
120     *
121     * @param string $name Cookie name
122     */
123    public function setName($name)
124    {
125        $this->data['Name'] = $name;
126    }
127
128    /**
129     * Get the cookie value
130     *
131     * @return string
132     */
133    public function getValue()
134    {
135        return $this->data['Value'];
136    }
137
138    /**
139     * Set the cookie value
140     *
141     * @param string $value Cookie value
142     */
143    public function setValue($value)
144    {
145        $this->data['Value'] = $value;
146    }
147
148    /**
149     * Get the domain
150     *
151     * @return string|null
152     */
153    public function getDomain()
154    {
155        return $this->data['Domain'];
156    }
157
158    /**
159     * Set the domain of the cookie
160     *
161     * @param string $domain
162     */
163    public function setDomain($domain)
164    {
165        $this->data['Domain'] = $domain;
166    }
167
168    /**
169     * Get the path
170     *
171     * @return string
172     */
173    public function getPath()
174    {
175        return $this->data['Path'];
176    }
177
178    /**
179     * Set the path of the cookie
180     *
181     * @param string $path Path of the cookie
182     */
183    public function setPath($path)
184    {
185        $this->data['Path'] = $path;
186    }
187
188    /**
189     * Maximum lifetime of the cookie in seconds
190     *
191     * @return int|null
192     */
193    public function getMaxAge()
194    {
195        return $this->data['Max-Age'];
196    }
197
198    /**
199     * Set the max-age of the cookie
200     *
201     * @param int $maxAge Max age of the cookie in seconds
202     */
203    public function setMaxAge($maxAge)
204    {
205        $this->data['Max-Age'] = $maxAge;
206    }
207
208    /**
209     * The UNIX timestamp when the cookie Expires
210     *
211     * @return mixed
212     */
213    public function getExpires()
214    {
215        return $this->data['Expires'];
216    }
217
218    /**
219     * Set the unix timestamp for which the cookie will expire
220     *
221     * @param int $timestamp Unix timestamp
222     */
223    public function setExpires($timestamp)
224    {
225        $this->data['Expires'] = is_numeric($timestamp)
226            ? (int) $timestamp
227            : strtotime($timestamp);
228    }
229
230    /**
231     * Get whether or not this is a secure cookie
232     *
233     * @return null|bool
234     */
235    public function getSecure()
236    {
237        return $this->data['Secure'];
238    }
239
240    /**
241     * Set whether or not the cookie is secure
242     *
243     * @param bool $secure Set to true or false if secure
244     */
245    public function setSecure($secure)
246    {
247        $this->data['Secure'] = $secure;
248    }
249
250    /**
251     * Get whether or not this is a session cookie
252     *
253     * @return null|bool
254     */
255    public function getDiscard()
256    {
257        return $this->data['Discard'];
258    }
259
260    /**
261     * Set whether or not this is a session cookie
262     *
263     * @param bool $discard Set to true or false if this is a session cookie
264     */
265    public function setDiscard($discard)
266    {
267        $this->data['Discard'] = $discard;
268    }
269
270    /**
271     * Get whether or not this is an HTTP only cookie
272     *
273     * @return bool
274     */
275    public function getHttpOnly()
276    {
277        return $this->data['HttpOnly'];
278    }
279
280    /**
281     * Set whether or not this is an HTTP only cookie
282     *
283     * @param bool $httpOnly Set to true or false if this is HTTP only
284     */
285    public function setHttpOnly($httpOnly)
286    {
287        $this->data['HttpOnly'] = $httpOnly;
288    }
289
290    /**
291     * Check if the cookie matches a path value
292     *
293     * @param string $path Path to check against
294     *
295     * @return bool
296     */
297    public function matchesPath($path)
298    {
299        return !$this->getPath() || 0 === stripos($path, $this->getPath());
300    }
301
302    /**
303     * Check if the cookie matches a domain value
304     *
305     * @param string $domain Domain to check against
306     *
307     * @return bool
308     */
309    public function matchesDomain($domain)
310    {
311        // Remove the leading '.' as per spec in RFC 6265.
312        // http://tools.ietf.org/html/rfc6265#section-5.2.3
313        $cookieDomain = ltrim($this->getDomain(), '.');
314
315        // Domain not set or exact match.
316        if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) {
317            return true;
318        }
319
320        // Matching the subdomain according to RFC 6265.
321        // http://tools.ietf.org/html/rfc6265#section-5.1.3
322        if (filter_var($domain, FILTER_VALIDATE_IP)) {
323            return false;
324        }
325
326        return (bool) preg_match('/\.' . preg_quote($cookieDomain) . '$/i', $domain);
327    }
328
329    /**
330     * Check if the cookie is expired
331     *
332     * @return bool
333     */
334    public function isExpired()
335    {
336        return $this->getExpires() !== null && time() > $this->getExpires();
337    }
338
339    /**
340     * Check if the cookie is valid according to RFC 6265
341     *
342     * @return bool|string Returns true if valid or an error message if invalid
343     */
344    public function validate()
345    {
346        // Names must not be empty, but can be 0
347        $name = $this->getName();
348        if (empty($name) && !is_numeric($name)) {
349            return 'The cookie name must not be empty';
350        }
351
352        // Check if any of the invalid characters are present in the cookie name
353        if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
354            return "Cookie name must not cannot invalid characters: =,; \\t\\r\\n\\013\\014";
355        }
356
357        // Value must not be empty, but can be 0
358        $value = $this->getValue();
359        if (empty($value) && !is_numeric($value)) {
360            return 'The cookie value must not be empty';
361        }
362
363        // Domains must not be empty, but can be 0
364        // A "0" is not a valid internet domain, but may be used as server name
365        // in a private network.
366        $domain = $this->getDomain();
367        if (empty($domain) && !is_numeric($domain)) {
368            return 'The cookie domain must not be empty';
369        }
370
371        return true;
372    }
373}
374