Passed
Push — master ( 3c979b...72c793 )
by Kirill
03:02
created

Session::getSection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Session;
13
14
use Spiral\Session\Exception\SessionException;
15
16
/**
17
 * Direct api to php session functionality with segmentation support. Automatically provides access
18
 * to _SESSION global variable and signs session with user signature.
19
 *
20
 * Session will be automatically started upon first request.
21
 *
22
 * @see  https://www.owasp.org/index.php/Session_Management_Cheat_Sheet
23
 */
24
final class Session implements SessionInterface
25
{
26
    /**
27
     * Signs every session with user specific hash, provides ability to fixate session.
28
     */
29
    private const CLIENT_SIGNATURE = '_CLIENT_SIGNATURE';
30
31
    /**
32
     * Time when session been created or refreshed.
33
     */
34
    private const SESSION_CREATED = '_CREATED';
35
36
    /**
37
     * Locations for unnamed segments i.e. default segment.
38
     */
39
    private const DEFAULT_SECTION = '_DEFAULT';
40
41
    /**
42
     * Unique string to identify client. Signature is stored inside the session.
43
     *
44
     * @var string
45
     */
46
    private $clientSignature;
47
48
    /**
49
     * Session lifetime in seconds.
50
     *
51
     * @var int
52
     */
53
    private $lifetime;
54
55
    /**
56
     * @var string
57
     */
58
    private $id = null;
59
60
    /**
61
     * @var bool
62
     */
63
    private $started = false;
64
65
    /**
66
     * @param string      $clientSignature
67
     * @param int         $lifetime
68
     * @param string|null $id
69
     */
70
    public function __construct(string $clientSignature, int $lifetime, string $id = null)
71
    {
72
        $this->clientSignature = $clientSignature;
73
        $this->lifetime = $lifetime;
74
75
        if (!empty($id) && $this->validID($id)) {
76
            $this->id = $id;
77
        }
78
    }
79
80
    /**
81
     * @return array
82
     */
83
    public function __debugInfo()
84
    {
85
        return [
86
            'id'        => $this->id,
87
            'signature' => $this->clientSignature,
88
            'started'   => $this->isStarted(),
89
            'data'      => $this->isStarted() ? $_SESSION : null
90
        ];
91
    }
92
93
    /**
94
     * @inheritdoc
95
     */
96
    public function isStarted(): bool
97
    {
98
        return $this->started;
99
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104
    public function resume(): void
105
    {
106
        if ($this->isStarted()) {
107
            return;
108
        }
109
110
        if (!empty($this->id)) {
111
            session_id($this->id);
112
        } else {
113
            // always new id
114
            session_id(session_create_id());
115
        }
116
117
        try {
118
            session_start(['use_cookies' => false]);
119
        } catch (\Throwable $e) {
120
            throw new SessionException('Unable to start session', $e->getCode(), $e);
121
        }
122
123
        if (empty($this->id)) {
124
            //Sign newly created session
125
            $_SESSION[self::CLIENT_SIGNATURE] = $this->clientSignature;
126
            $_SESSION[self::SESSION_CREATED] = time();
127
        }
128
129
        //We got new session
130
        $this->id = session_id();
131
        $this->started = true;
132
133
        //Ensure that session is valid
134
        if (!$this->validSession()) {
135
            $this->invalidateSession();
136
        }
137
    }
138
139
    /**
140
     * @inheritdoc
141
     */
142
    public function getID(): ?string
143
    {
144
        return $this->id;
145
    }
146
147
    /**
148
     * @inheritdoc
149
     */
150
    public function regenerateID(): SessionInterface
151
    {
152
        $this->resume();
153
154
        //Gaining new ID
155
        session_regenerate_id();
156
        $this->id = session_id();
157
158
        //Updating session duration
159
        $_SESSION[self::SESSION_CREATED] = time();
160
        session_write_close();
161
162
        //Restarting session under new ID
163
        $this->resume();
164
165
        return $this;
166
    }
167
168
    /**
169
     * @inheritdoc
170
     */
171
    public function commit(): bool
172
    {
173
        if (!$this->isStarted()) {
174
            return false;
175
        }
176
177
        session_write_close();
178
        $this->started = false;
179
180
        return true;
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186
    public function abort(): bool
187
    {
188
        if (!$this->isStarted()) {
189
            return false;
190
        }
191
192
        session_abort();
193
        $this->started = false;
194
195
        return true;
196
    }
197
198
199
    /**
200
     * @inheritdoc
201
     */
202
    public function destroy(): bool
203
    {
204
        $this->resume();
205
        $_SESSION = [
206
            self::CLIENT_SIGNATURE => $this->clientSignature,
207
            self::SESSION_CREATED  => time()
208
        ];
209
210
        return $this->commit();
211
    }
212
213
    /**
214
     * @inheritdoc
215
     */
216
    public function getSection(string $name = null): SessionSectionInterface
217
    {
218
        return new SessionSection($this, $name ?? static::DEFAULT_SECTION);
219
    }
220
221
    /**
222
     * Check if session is valid for
223
     *
224
     * @return bool
225
     */
226
    protected function validSession(): bool
227
    {
228
        if (
229
            !array_key_exists(self::CLIENT_SIGNATURE, $_SESSION)
230
            || !array_key_exists(self::SESSION_CREATED, $_SESSION)
231
        ) {
232
            //Missing session signature or timestamp!
233
            return false;
234
        }
235
236
        if ($_SESSION[self::SESSION_CREATED] < time() - $this->lifetime) {
237
            //Session expired
238
            return false;
239
        }
240
241
        if (!hash_equals($_SESSION[self::CLIENT_SIGNATURE], $this->clientSignature)) {
242
            //Signatures do not match
243
            return false;
244
        }
245
246
        return true;
247
    }
248
249
    /**
250
     * To be called in cases when client does not supplied proper session signature.
251
     */
252
    protected function invalidateSession(): void
253
    {
254
        //Destroy all session data
255
        $this->destroy();
256
257
        //Switch user to new session
258
        $this->regenerateID();
259
    }
260
261
    /**
262
     * Check if given session ID valid.
263
     *
264
     * @param string $id
265
     * @return bool
266
     */
267
    private function validID(string $id): bool
268
    {
269
        return preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $id) !== false;
270
    }
271
}
272