Test Setup Failed
Push — master ( 210134...c17796 )
by Damian
03:18
created

src/Control/CookieJar.php (3 issues)

Labels
Severity
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\ORM\FieldType\DBDatetime;
6
use LogicException;
7
8
/**
9
 * A default backend for the setting and getting of cookies
10
 *
11
 * This backend allows one to better test Cookie setting and separate cookie
12
 * handling from the core
13
 *
14
 * @todo Create a config array for defaults (eg: httpOnly, secure, path, domain, expiry)
15
 * @todo A getter for cookies that haven't been sent to the browser yet
16
 * @todo Tests / a way to set the state without hacking with $_COOKIE
17
 * @todo Store the meta information around cookie setting (path, domain, secure, etc)
18
 */
19
class CookieJar implements Cookie_Backend
20
{
21
22
    /**
23
     * Hold the cookies that were existing at time of instantiation (ie: The ones
24
     * sent to PHP by the browser)
25
     *
26
     * @var array Existing cookies sent by the browser
27
     */
28
    protected $existing = array();
29
30
    /**
31
     * Hold the current cookies (ie: a mix of those that were sent to us and we
32
     * have set without the ones we've cleared)
33
     *
34
     * @var array The state of cookies once we've sent the response
35
     */
36
    protected $current = array();
37
38
    /**
39
     * Hold any NEW cookies that were set by the application and will be sent
40
     * in the next response
41
     *
42
     * @var array New cookies set by the application
43
     */
44
    protected $new = array();
45
46
    /**
47
     * When creating the backend we want to store the existing cookies in our
48
     * "existing" array. This allows us to distinguish between cookies we received
49
     * or we set ourselves (and didn't get from the browser)
50
     *
51
     * @param array $cookies The existing cookies to load into the cookie jar.
52
     * Omit this to default to $_COOKIE
53
     */
54
    public function __construct($cookies = array())
55
    {
56
        $this->current = $this->existing = func_num_args()
57
            ? ($cookies ?: array()) // Convert empty values to blank arrays
58
            : $_COOKIE;
59
    }
60
61
    /**
62
     * Set a cookie
63
     *
64
     * @param string $name The name of the cookie
65
     * @param string $value The value for the cookie to hold
66
     * @param int $expiry The number of days until expiry; 0 indicates a cookie valid for the current session
67
     * @param string $path The path to save the cookie on (falls back to site base)
68
     * @param string $domain The domain to make the cookie available on
69
     * @param boolean $secure Can the cookie only be sent over SSL?
70
     * @param boolean $httpOnly Prevent the cookie being accessible by JS
71
     */
72
    public function set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = true)
73
    {
74
        //are we setting or clearing a cookie? false values are reserved for clearing cookies (see PHP manual)
75
        $clear = false;
76
        if ($value === false || $value === '' || $expiry < 0) {
77
            $clear = true;
78
            $value = false;
79
        }
80
81
        //expiry === 0 is a special case where we set a cookie for the current user session
82
        if ($expiry !== 0) {
83
            //don't do the maths if we are clearing
84
            $expiry = $clear ? -1 : DBDatetime::now()->getTimestamp() + (86400 * $expiry);
85
        }
86
        //set the path up
87
        $path = $path ? $path : Director::baseURL();
88
        //send the cookie
89
        $this->outputCookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly);
0 ignored issues
show
It seems like $value can also be of type false; however, parameter $value of SilverStripe\Control\CookieJar::outputCookie() does only seem to accept string|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
        $this->outputCookie($name, /** @scrutinizer ignore-type */ $value, $expiry, $path, $domain, $secure, $httpOnly);
Loading history...
90
        //keep our variables in check
91
        if ($clear) {
92
            unset($this->new[$name], $this->current[$name]);
93
        } else {
94
            $this->new[$name] = $this->current[$name] = $value;
95
        }
96
    }
97
98
    /**
99
     * Get the cookie value by name
100
     *
101
     * Cookie names are normalised to work around PHP's behaviour of replacing incoming variable name . with _
102
     *
103
     * @param string $name The name of the cookie to get
104
     * @param boolean $includeUnsent Include cookies we've yet to send when fetching values
105
     *
106
     * @return string|null The cookie value or null if unset
107
     */
108
    public function get($name, $includeUnsent = true)
109
    {
110
        $cookies = $includeUnsent ? $this->current : $this->existing;
111
        if (isset($cookies[$name])) {
112
            return $cookies[$name];
113
        }
114
115
        //Normalise cookie names by replacing '.' with '_'
116
        $safeName = str_replace('.', '_', $name);
117
        if (isset($cookies[$safeName])) {
118
            return $cookies[$safeName];
119
        }
120
        return null;
121
    }
122
123
    /**
124
     * Get all the cookies
125
     *
126
     * @param boolean $includeUnsent Include cookies we've yet to send
127
     * @return array All the cookies
128
     */
129
    public function getAll($includeUnsent = true)
130
    {
131
        return $includeUnsent ? $this->current : $this->existing;
132
    }
133
134
    /**
135
     * Force the expiry of a cookie by name
136
     *
137
     * @param string $name The name of the cookie to expire
138
     * @param string $path The path to save the cookie on (falls back to site base)
139
     * @param string $domain The domain to make the cookie available on
140
     * @param boolean $secure Can the cookie only be sent over SSL?
141
     * @param boolean $httpOnly Prevent the cookie being accessible by JS
142
     */
143
    public function forceExpiry($name, $path = null, $domain = null, $secure = false, $httpOnly = true)
144
    {
145
        $this->set($name, false, -1, $path, $domain, $secure, $httpOnly);
0 ignored issues
show
false of type false is incompatible with the type string expected by parameter $value of SilverStripe\Control\CookieJar::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

145
        $this->set($name, /** @scrutinizer ignore-type */ false, -1, $path, $domain, $secure, $httpOnly);
Loading history...
146
    }
147
148
    /**
149
     * The function that actually sets the cookie using PHP
150
     *
151
     * @see http://uk3.php.net/manual/en/function.setcookie.php
152
     *
153
     * @param string $name The name of the cookie
154
     * @param string|array $value The value for the cookie to hold
155
     * @param int $expiry The number of days until expiry
156
     * @param string $path The path to save the cookie on (falls back to site base)
157
     * @param string $domain The domain to make the cookie available on
158
     * @param boolean $secure Can the cookie only be sent over SSL?
159
     * @param boolean $httpOnly Prevent the cookie being accessible by JS
160
     * @return boolean If the cookie was set or not; doesn't mean it's accepted by the browser
161
     */
162
    protected function outputCookie(
163
        $name,
164
        $value,
165
        $expiry = 90,
166
        $path = null,
167
        $domain = null,
168
        $secure = false,
169
        $httpOnly = true
170
    ) {
171
        // if headers aren't sent, we can set the cookie
172
        if (!headers_sent($file, $line)) {
173
            return setcookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly);
0 ignored issues
show
It seems like $value can also be of type array; however, parameter $value of setcookie() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

173
            return setcookie($name, /** @scrutinizer ignore-type */ $value, $expiry, $path, $domain, $secure, $httpOnly);
Loading history...
174
        }
175
176
        if (Cookie::config()->uninherited('report_errors')) {
177
            throw new LogicException(
178
                "Cookie '$name' can't be set. The site started outputting content at line $line in $file"
179
            );
180
        }
181
        return false;
182
    }
183
}
184