Completed
Push — master ( 5a5c3b...ec8644 )
by
unknown
11s
created

CookieStore::write()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 52
rs 8.425
c 0
b 0
f 0
cc 6
nc 3
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            || !($crypto = $this->getCrypto($session_id))
104
        ) {
105
            return;
106
        }
107
108
        // Decrypt and invalidate old data
109
        $cookieData = $crypto->decrypt($this->currentCookieData);
110
        $this->currentCookieData = null;
111
112
        // Verify expiration
113
        if ($cookieData) {
114
            $expiry = (int)substr($cookieData, 0, 10);
115
            $data = substr($cookieData, 10);
116
117
            if ($expiry > $this->getNow()) {
118
                return $data;
119
            }
120
        }
121
    }
122
123
    /**
124
     * Determine if the session could be verifably written to cookie storage
125
     *
126
     * @return bool
127
     */
128
    protected function canWrite()
129
    {
130
        return !headers_sent();
131
    }
132
133
    public function write($session_id, $session_data)
134
    {
135
        $canWrite = $this->canWrite();
136
        $isExceedingCookieLimit = (strlen($session_data) > static::config()->get('max_length'));
137
        $crypto = $this->getCrypto($session_id);
138
139
        // Check ability to safely encrypt and write content
140
        if (!$canWrite || $isExceedingCookieLimit || !$crypto) {
141
            if ($canWrite && $isExceedingCookieLimit) {
142
                $params = session_get_cookie_params();
143
                // Clear stored cookie value and cookie when length exceeds the set limit
144
                $this->currentCookieData = null;
145
                Cookie::set(
146
                    $this->cookie,
147
                    '',
148
                    0,
149
                    $params['path'],
150
                    $params['domain'],
151
                    $params['secure'],
152
                    $params['httponly']
153
                );
154
            }
155
156
            return false;
157
        }
158
159
        // Prepare content for write
160
        $params = session_get_cookie_params();
161
        // Total max lifetime, stored internally
162
        $lifetime = $this->getLifetime();
163
        $expiry = $this->getNow() + $lifetime;
164
165
        // Restore the known good cookie value
166
        $this->currentCookieData = $this->crypto->encrypt(
167
            sprintf('%010u', $expiry) . $session_data
168
        );
169
170
        // Respect auto-expire on browser close for the session cookie (in case the cookie lifetime is zero)
171
        $cookieLifetime = min((int)$params['lifetime'], $lifetime);
172
173
        Cookie::set(
174
            $this->cookie,
175
            $this->currentCookieData,
176
            $cookieLifetime / 86400,
177
            $params['path'],
178
            $params['domain'],
179
            $params['secure'],
180
            $params['httponly']
181
        );
182
183
        return true;
184
    }
185
186
    public function destroy($session_id)
187
    {
188
        $this->currentCookieData = null;
189
190
        $params = session_get_cookie_params();
191
192
        Cookie::force_expiry(
193
            $this->cookie,
194
            $params['path'],
195
            $params['domain'],
196
            $params['secure'],
197
            $params['httponly']
198
        );
199
    }
200
201
    public function gc($maxlifetime)
202
    {
203
        // NOP
204
    }
205
}
206