Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
91 / 91
100.00% covered (success)
100.00%
25 / 25
CRAP
100.00% covered (success)
100.00%
1 / 1
Cookie
100.00% covered (success)
100.00%
91 / 91
100.00% covered (success)
100.00%
25 / 25
55
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInstance
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getOptions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 setOptions
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
12
 set
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 getExpires
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDomain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSecure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isHttpOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSamesite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 clear
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __set
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Cookie;
16
17use ArrayIterator;
18
19/**
20 * Cookie class
21 *
22 * @category   Pop
23 * @package    Pop\Cookie
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.0.0
28 */
29class Cookie implements \ArrayAccess, \Countable, \IteratorAggregate
30{
31
32    /**
33     * Instance of the cookie object
34     * @var Cookie
35     */
36    static private Cookie $instance;
37
38    /**
39     * Cookie IP
40     * @var ?string
41     */
42    private ?string $ip = null;
43
44    /**
45     * Cookie Expiration
46     * @var int
47     */
48    private int $expires = 0;
49
50    /**
51     * Cookie Path
52     * @var string
53     */
54    private string $path = '/';
55
56    /**
57     * Cookie Domain
58     * @var ?string
59     */
60    private ?string $domain = null;
61
62    /**
63     * Cookie Secure Flag
64     * @var bool
65     */
66    private bool $secure = false;
67
68    /**
69     * Cookie HTTP Only Flag
70     * @var bool
71     */
72    private bool $httponly = false;
73
74    /**
75     * Cookie SameSite Flag (None, Lax, Strict)
76     * @var string
77     */
78    private string $samesite = 'Lax';
79
80    /**
81     * Constructor
82     *
83     * Private method to instantiate the cookie object
84     *
85     * @param  array $options
86     */
87    private function __construct(array $options = [])
88    {
89        $this->setOptions($options);
90    }
91
92    /**
93     * Determine whether or not an instance of the cookie object exists
94     * already, and instantiate the object if it does not exist.
95     *
96     * @param  array $options
97     * @return Cookie
98     */
99    public static function getInstance(array $options = []): Cookie
100    {
101        if (empty(self::$instance)) {
102            self::$instance = new Cookie($options);
103        } else if (!empty($options)) {
104            self::$instance->setOptions($options);
105        }
106
107        return self::$instance;
108    }
109
110    /**
111     * Method to create options array
112     *
113     * @return array
114     */
115    public function getOptions(): array
116    {
117        return [
118            'expires'  => $this->expires,
119            'path'     => $this->path,
120            'domain'   => $this->domain,
121            'secure'   => $this->secure,
122            'httponly' => $this->httponly,
123            'samesite' => $this->samesite
124        ];
125    }
126
127    /**
128     * Private method to set options
129     *
130     * @param  array $options
131     * @throws Exception
132     * @return Cookie
133     */
134    public function setOptions(array $options = []): Cookie
135    {
136        // Set the cookie owner's IP address and domain.
137        $this->ip = $_SERVER['REMOTE_ADDR'] ?? null;
138
139        if (isset($_SERVER['SERVER_NAME'])) {
140            $this->domain = $_SERVER['SERVER_NAME'];
141        } else if (isset($_SERVER['HTTP_HOST'])) {
142            $this->domain = $_SERVER['HTTP_HOST'];
143        }
144
145        $expires  = isset($options['expires']) ? (int)$options['expires'] : $this->expires;
146        $path     = $options['path'] ?? $this->path;
147        $domain   = $options['domain'] ?? $this->domain;
148        $secure   = isset($options['secure']) ? (bool)$options['secure'] : $this->secure;
149        $httponly = isset($options['httponly']) ? (bool)$options['httponly'] : $this->httponly;
150        $samesite = $this->samesite;
151
152        if (isset($options['samesite'])) {
153            if (($options['samesite'] != 'None') && ($options['samesite'] != 'Lax') && ($options['samesite'] != 'Strict')) {
154                throw new Exception("Error: The 'samesite' option must be 'None', 'Lax' or 'Strict'.");
155            }
156            $samesite = $options['samesite'];
157        }
158
159        if (($samesite == 'None') && ($secure === false)) {
160            throw new Exception("Error: A 'samesite' value of 'None' requires the 'secure' option to be set to true.");
161        }
162
163        // Only commit the new state once every option above has validated successfully.
164        $this->expires  = $expires;
165        $this->path     = $path;
166        $this->domain   = $domain;
167        $this->secure   = $secure;
168        $this->httponly = $httponly;
169        $this->samesite = $samesite;
170
171        return $this;
172    }
173
174    /**
175     * Set a cookie
176     *
177     * @param  string  $name
178     * @param  mixed   $value
179     * @param  array   $options
180     * @throws Exception
181     * @return Cookie
182     */
183    public function set(string $name, mixed $value, array $options = []): Cookie
184    {
185        if (!empty($options)) {
186            $this->setOptions($options);
187        }
188
189        if (!is_string($value) && !is_numeric($value)) {
190            $value = json_encode($value);
191        }
192
193        if (setcookie($name, (string)$value, $this->getOptions()) === false) {
194            throw new Exception("Error: Unable to set the cookie '" . $name . "'.");
195        }
196
197        return $this;
198    }
199
200    /**
201     * Return the current cookie expiration
202     *
203     * @return int
204     */
205    public function getExpires(): int
206    {
207        return $this->expires;
208    }
209
210    /**
211     * Return the current cookie path.
212     *
213     * @return string
214     */
215    public function getPath(): string
216    {
217        return $this->path;
218    }
219
220    /**
221     * Return the current cookie domain
222     *
223     * @return string|null
224     */
225    public function getDomain(): string|null
226    {
227        return $this->domain;
228    }
229
230    /**
231     * Return if the cookie is secure
232     *
233     * @return bool
234     */
235    public function isSecure(): bool
236    {
237        return $this->secure;
238    }
239
240    /**
241     * Return if the cookie is HTTP only
242     *
243     * @return bool
244     */
245    public function isHttpOnly(): bool
246    {
247        return $this->httponly;
248    }
249
250    /**
251     * Return if the cookie's samesite flag
252     *
253     * @return string
254     */
255    public function getSamesite(): string
256    {
257        return $this->samesite;
258    }
259
260    /**
261     * Return the current IP address.
262     *
263     * @return string|null
264     */
265    public function getIp(): string|null
266    {
267        return $this->ip;
268    }
269
270    /**
271     * Delete a cookie
272     *
273     * @param  string $name
274     * @param  array  $options
275     * @throws Exception
276     * @return void
277     */
278    public function delete(string $name, array $options = []): void
279    {
280        if (!empty($options)) {
281            $this->setOptions($options);
282        }
283        if (isset($_COOKIE[$name])) {
284            $this->expires = time() - 3600;
285            if (setcookie($name, (string)$_COOKIE[$name], $this->getOptions()) === false) {
286                throw new Exception("Error: Unable to delete the cookie '" . $name . "'.");
287            }
288        }
289    }
290
291    /**
292     * Clear (delete) all cookies
293     *
294     * @param  array $options
295     * @throws Exception
296     * @return void
297     */
298    public function clear(array $options = []): void
299    {
300        if (!empty($options)) {
301            $this->setOptions($options);
302        }
303
304        $this->expires = time() - 3600;
305
306        foreach ($_COOKIE as $name => $value) {
307            if (isset($_COOKIE[$name])) {
308                if (setcookie($name, (string)$_COOKIE[$name], $this->getOptions()) === false) {
309                    throw new Exception("Error: Unable to clear the cookie '" . $name . "'.");
310                }
311            }
312        }
313    }
314
315    /**
316     * Method to get the count of cookie data
317     *
318     * @return int
319     */
320    public function count(): int
321    {
322        return count($this->toArray());
323    }
324    /**
325     * Method to iterate over the cookie
326     *
327     * @return ArrayIterator
328     */
329    public function getIterator(): ArrayIterator
330    {
331        return new ArrayIterator($this->toArray());
332    }
333    /**
334     * Get the cookie values as an array
335     *
336     * @return array
337     */
338    public function toArray(): array
339    {
340        return $_COOKIE;
341    }
342
343    /**
344     * Set method to set the value of the $_COOKIE global variable
345     *
346     * @param  string $name
347     * @param  mixed $value
348     * @return void
349     */
350    public function __set(string $name, mixed $value)
351    {
352        $options = [
353            'expires'  => $this->expires,
354            'path'     => $this->path,
355            'domain'   => $this->domain,
356            'secure'   => $this->secure,
357            'httponly' => $this->httponly
358        ];
359        $this->set($name, $value, $options);
360    }
361
362    /**
363     * Get method to return the value of the $_COOKIE global variable
364     *
365     * @param  string $name
366     * @return mixed
367     */
368    public function __get(string $name): mixed
369    {
370        $value = null;
371        if (isset($_COOKIE[$name])) {
372            $raw   = $_COOKIE[$name];
373            $value = (str_starts_with($raw, '{') || str_starts_with($raw, '[') || in_array($raw, ['true', 'false', 'null'], true)) ?
374                json_decode($raw, true) : $raw;
375        }
376        return $value;
377    }
378
379    /**
380     * Return the isset value of the $_COOKIE global variable
381     *
382     * @param  string $name
383     * @return bool
384     */
385    public function __isset(string $name): bool
386    {
387        return isset($_COOKIE[$name]);
388    }
389
390    /**
391     * Unset the value in the $_COOKIE global variable
392     *
393     * @param  string $name
394     * @throws Exception
395     * @return void
396     */
397    public function __unset(string $name): void
398    {
399        if (isset($_COOKIE[$name])) {
400            $this->expires = time() - 3600;
401            if (setcookie($name, (string)$_COOKIE[$name], $this->getOptions()) === false) {
402                throw new Exception("Error: Unable to unset the cookie '" . $name . "'.");
403            }
404        }
405    }
406
407    /**
408     * ArrayAccess offsetSet
409     *
410     * @param  mixed $offset
411     * @param  mixed $value
412     * @return void
413     */
414    public function offsetSet(mixed $offset, mixed $value): void
415    {
416        $this->__set($offset, $value);
417    }
418
419    /**
420     * ArrayAccess offsetGet
421     *
422     * @param  mixed $offset
423     * @return mixed
424     */
425    public function offsetGet(mixed $offset): mixed
426    {
427        return $this->__get($offset);
428    }
429
430    /**
431     * ArrayAccess offsetExists
432     *
433     * @param  mixed $offset
434     * @return bool
435     */
436    public function offsetExists(mixed $offset): bool
437    {
438        return $this->__isset($offset);
439    }
440
441    /**
442     * ArrayAccess offsetUnset
443     *
444     * @param  mixed $offset
445     * @return void
446     */
447    public function offsetUnset(mixed $offset): void
448    {
449        $this->__unset($offset);
450    }
451
452}