CookieStore::destroy()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace SilverStripe\HybridSessions\Store;
4
5
use SilverStripe\Control\Cookie;
6
use SilverStripe\HybridSessions\Crypto\CryptoHandler;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Injector\Injector;
9
10
/**
11
 * A session store which stores the session data in an encrypted & signed cookie.
12
 *
13
 * This way the server doesn't need to open a database connection or have a shared filesystem for reading
14
 * the session from - the client passes through the session with every request.
15
 *
16
 * This approach does have some limitations - cookies can only be quite small (4K total, but we limit to 1K)
17
 * and can only be set _before_ the server starts sending a response.
18
 *
19
 * So we clear the cookie on Session startup (which should always be before the headers get sent), but just
20
 * fail on Session write if we can't use cookies, assuming there's something watching for that & providing a fallback
21
 */
22
class CookieStore extends BaseStore
23
{
24
25
    /**
26
     * Maximum length of a cookie value in characters
27
     *
28
     * @var int
29
     * @config
30
     */
31
    private static $max_length = 1024;
32
33
    /**
34
     * Encryption service
35
     *
36
     * @var HybridSessionStore_Crypto
37
     */
38
    protected $crypto;
39
40
    /**
41
     * Name of cookie
42
     *
43
     * @var string
44
     */
45
    protected $cookie;
46
47
    /**
48
     * Known unmodified value of this cookie. If the cookie backend has been read into the application,
49
     * then the backend is unable to verify the modification state of this value internally within the
50
     * system, so this will be left null unless written back.
51
     *
52
     * If the content exceeds max_length then the backend can also not maintain this cookie, also
53
     * setting this variable to null.
54
     *
55
     * @var string
56
     */
57
    protected $currentCookieData;
58
59
    public function open($save_path, $name)
60
    {
61
        $this->cookie = $name . '_2';
62
63
        // Read the incoming value, then clear the cookie - we might not be able
64
        // to do so later if write() is called after headers are sent
65
        // This is intended to force a failover to the database store if the
66
        // modified session cannot be emitted.
67
        $this->currentCookieData = Cookie::get($this->cookie);
68
69
        if ($this->currentCookieData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->currentCookieData of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
70
            Cookie::set($this->cookie, '');
71
        }
72
    }
73
74
    public function close()
75
    {
76
    }
77
78
    /**
79
     * Get the cryptography store for the specified session
80
     *
81
     * @param string $session_id
82
     * @return HybridSessionStore_Crypto
83
     */
84
    protected function getCrypto($session_id)
85
    {
86
        $key = $this->getKey();
87
88
        if (!$key) {
89
            return null;
90
        }
91
92
        if (!$this->crypto || $this->crypto->getSalt() != $session_id) {
93
            $this->crypto = Injector::inst()->create(CryptoHandler::class, $key, $session_id);
94
        }
95
96
        return $this->crypto;
97
    }
98
99
    public function read($session_id)
100
    {
101
        // Check ability to safely decrypt content
102
        if (!$this->currentCookieData) {
103
            return;
104
        }
105
106
        $crypto = $this->getCrypto($session_id);
107
        if (!$crypto) {
108
            return;
109
        }
110
111
        // Decrypt and invalidate old data
112
        $cookieData = $crypto->decrypt($this->currentCookieData);
113
        $this->currentCookieData = null;
114
115
        // Verify expiration
116
        if ($cookieData) {
117
            $expiry = (int)substr($cookieData, 0, 10);
118
            $data = substr($cookieData, 10);
119
120
            if ($expiry > $this->getNow()) {
121
                return $data;
122
            }
123
        }
124
    }
125
126
    /**
127
     * Determine if the session could be verifably written to cookie storage
128
     *
129
     * @return bool
130
     */
131
    protected function canWrite()
132
    {
133
        return !headers_sent();
134
    }
135
136
    public function write($session_id, $session_data)
137
    {
138
        $canWrite = $this->canWrite();
139
        $isExceedingCookieLimit = (strlen($session_data) > static::config()->get('max_length'));
140
        $crypto = $this->getCrypto($session_id);
141
142
        // Check ability to safely encrypt and write content
143
        if (!$canWrite || $isExceedingCookieLimit || !$crypto) {
144
            if ($canWrite && $isExceedingCookieLimit) {
145
                $params = session_get_cookie_params();
146
                // Clear stored cookie value and cookie when length exceeds the set limit
147
                $this->currentCookieData = null;
148
                Cookie::set(
149
                    $this->cookie,
150
                    '',
151
                    0,
152
                    $params['path'],
153
                    $params['domain'],
154
                    $params['secure'],
155
                    $params['httponly']
156
                );
157
            }
158
159
            return false;
160
        }
161
162
        // Prepare content for write
163
        $params = session_get_cookie_params();
164
        // Total max lifetime, stored internally
165
        $lifetime = $this->getLifetime();
166
        $expiry = $this->getNow() + $lifetime;
167
168
        // Restore the known good cookie value
169
        $this->currentCookieData = $this->crypto->encrypt(
170
            sprintf('%010u', $expiry) . $session_data
171
        );
172
173
        // Respect auto-expire on browser close for the session cookie (in case the cookie lifetime is zero)
174
        $cookieLifetime = min((int)$params['lifetime'], $lifetime);
175
176
        Cookie::set(
177
            $this->cookie,
178
            $this->currentCookieData,
179
            $cookieLifetime / 86400,
180
            $params['path'],
181
            $params['domain'],
182
            $params['secure'],
183
            $params['httponly']
184
        );
185
186
        return true;
187
    }
188
189
    public function destroy($session_id)
190
    {
191
        $this->currentCookieData = null;
192
193
        $params = session_get_cookie_params();
194
195
        Cookie::force_expiry(
196
            $this->cookie,
197
            $params['path'],
198
            $params['domain'],
199
            $params['secure'],
200
            $params['httponly']
201
        );
202
    }
203
204
    public function gc($maxlifetime)
205
    {
206
        // NOP
207
    }
208
}
209