Completed
Pull Request — master (#47)
by
unknown
01:29
created

CookieStore::write()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 51
rs 8.4468
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
        // Check ability to safely encrypt and write content
136
        if (!$this->canWrite()
137
            || (strlen($session_data) > static::config()->get('max_length'))
138
            || !($crypto = $this->getCrypto($session_id))
139
        ) {
140
            if ($this->canWrite() && strlen($session_data) > static::config()->get('max_length')) {
141
                $params = session_get_cookie_params();
142
                // Clear stored cookie value and cookie when length exceeds the set limit
143
                $this->currentCookieData = null;
144
                Cookie::set(
145
                    $this->cookie,
146
                    '',
147
                    0,
148
                    $params['path'],
149
                    $params['domain'],
150
                    $params['secure'],
151
                    $params['httponly']
152
                );
153
            }
154
155
            return false;
156
        }
157
158
        // Prepare content for write
159
        $params = session_get_cookie_params();
160
        // Total max lifetime, stored internally
161
        $lifetime = $this->getLifetime();
162
        $expiry = $this->getNow() + $lifetime;
163
164
        // Restore the known good cookie value
165
        $this->currentCookieData = $this->crypto->encrypt(
166
            sprintf('%010u', $expiry) . $session_data
167
        );
168
169
        // Respect auto-expire on browser close for the session cookie (in case the cookie lifetime is zero)
170
        $cookieLifetime = min((int)$params['lifetime'], $lifetime);
171
172
        Cookie::set(
173
            $this->cookie,
174
            $this->currentCookieData,
175
            $cookieLifetime / 86400,
176
            $params['path'],
177
            $params['domain'],
178
            $params['secure'],
179
            $params['httponly']
180
        );
181
182
        return true;
183
    }
184
185
    public function destroy($session_id)
186
    {
187
        $this->currentCookieData = null;
188
189
        $params = session_get_cookie_params();
190
191
        Cookie::force_expiry(
192
            $this->cookie,
193
            $params['path'],
194
            $params['domain'],
195
            $params['secure'],
196
            $params['httponly']
197
        );
198
    }
199
200
    public function gc($maxlifetime)
201
    {
202
        // NOP
203
    }
204
}
205