Passed
Push — master ( 994183...c8a212 )
by Derek Stephen
09:25 queued 01:07
created

SessionManager   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Importance

Changes 11
Bugs 2 Features 3
Metric Value
eloc 62
c 11
b 2
f 3
dl 0
loc 236
rs 9.6
wmc 35

16 Methods

Rating   Name   Duplication   Size   Complexity  
A shouldRandomlyRegenerate() 0 3 1
A isValid() 0 11 5
A sessionStart() 0 20 5
A __construct() 0 2 1
A regenerateSession() 0 25 3
A __clone() 0 2 1
A getIpAddress() 0 5 1
A has() 0 3 1
A getInstance() 0 9 2
A get() 0 3 2
A destroySession() 0 8 2
A unset() 0 3 1
A initialise() 0 17 3
A destroy() 0 3 1
A isHijackAttempt() 0 18 5
A set() 0 3 1
1
<?php declare(strict_types=1);
2
3
namespace Del;
4
5
final class SessionManager
6
{
7
    const IP_REGEX = '/(\d{1,3}\.\d{1,3}\.\d{1,3}\.)(\d{1,3})/';
8
9
    /**
10
     *  As this is a singleton, construction and clone are disabled
11
     *  use SessionManager::getInstance() if you need the instance
12
     */
13
    private function __construct()
14
    {
15
    }
16
17
    private function __clone()
18
    {
19
    }
20
21
    /**
22
     * @return SessionManager
23
     */
24
    public static function getInstance(): SessionManager
25
    {
26
        static $inst = null;
27
28
        if ($inst === null) {
29
            $inst = new SessionManager();
30
        }
31
32
        return $inst;
33
    }
34
35
    /**
36
     * Creates a secure session
37
     *
38
     * @param string $name
39
     * @param int $lifetime
40
     * @param string $path
41
     * @param string $domain
42
     * @param bool|null $secure
43
     */
44
    public static function sessionStart(string $name, int $lifetime = 0, string $path = '/', string $domain = '', ?bool $secure = null): void
45
    {
46
        // get the instance of the session manager
47
        $inst = self::getInstance();
48
49
        // Set the domain to default to the current domain.
50
        $domain = isset($domain) ? $domain : $_SERVER['SERVER_NAME'];
51
52
        // Set the default secure value to whether the site is being accessed with SSL
53
        $secure = isset($secure) ? $secure : isset($_SERVER['HTTPS']);
54
        $id = session_id();
55
56
        if (empty($id)) {
57
            session_name($name . '_Session');
58
            session_set_cookie_params($lifetime, $path, $domain, $secure, true);
59
            session_start();
60
        }
61
62
        // Make sure the session hasn't expired, and destroy it if it has
63
        $inst->isValid() ? $inst->initialise() :  $inst->destroySession();
64
65
    }
66
67
    private function initialise(): void
68
    {
69
        // Check to see if the session is a hijacking attempt
70
        if ($this->isHijackAttempt()) {
71
72
            // Reset session data and regenerate id
73
            $_SESSION = [];
74
            $_SESSION['ipAddress'] = $this->getIpAddress();
75
            $_SESSION['userAgent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
76
            $this->regenerateSession();
77
78
            return;
79
        }
80
81
        // Give a 5% chance of the session id changing on any request
82
        if ($this->shouldRandomlyRegenerate()) {
83
            $this->regenerateSession();
84
        }
85
    }
86
87
    /**
88
     * @return bool
89
     */
90
    private function shouldRandomlyRegenerate(): bool
91
    {
92
        return rand(1, 100) <= 5;
93
    }
94
95
96
    /**
97
     * Checks session IP and user agent are still the same
98
     * @return bool
99
     */
100
    private function isHijackAttempt(): bool
101
    {
102
        $ipAddress = $this->getIpAddress();
103
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
104
105
        if (!isset($_SESSION['ipAddress']) || !isset($_SESSION['userAgent'])) {
106
            return true;
107
        }
108
109
        if ($_SESSION['ipAddress'] != $ipAddress) {
110
            return true;
111
        }
112
113
        if ($_SESSION['userAgent'] !== $userAgent) {
114
            return true;
115
        }
116
117
        return false;
118
    }
119
120
    /**
121
     * If a site goes through the likes of Cloudflare, the last part of the IP might change
122
     * So we replace it with an x.
123
     *
124
     * @return string
125
     */
126
    private function getIpAddress(): string
127
    {
128
        $remoteAddress = $_SERVER['REMOTE_ADDR'] ?? '';
129
130
        return preg_replace(self::IP_REGEX, '$1x', $remoteAddress);
131
    }
132
133
    /**
134
     *  Creates a fresh session Id to make it harder to hack
135
     *  If the site is very slow in parts increase the expiry time
136
     *  10 seconds is a good default which allows ajax calls to work
137
     *  without losing the session
138
     */
139
    private function regenerateSession()
140
    {
141
        // If this session is obsolete it means there already is a new id
142
        if (isset($_SESSION['OBSOLETE']) && $_SESSION['OBSOLETE'] == true) {
143
            return;
144
        }
145
146
        // Set current session to expire in 10 seconds
147
        $_SESSION['OBSOLETE'] = true;
148
        $_SESSION['EXPIRES'] = time() + 10;
149
150
        // Create new session without destroying the old one
151
        session_regenerate_id(false);
152
153
        // Grab current session ID and close both sessions to allow other scripts to use them
154
        $newSession = session_id();
155
        session_write_close();
156
157
        // Set session ID to the new one, and start it back up again
158
        session_id($newSession);
159
        session_start();
160
161
        // Now we unset the obsolete and expiration values for the session we want to keep
162
        unset($_SESSION['OBSOLETE']);
163
        unset($_SESSION['EXPIRES']);
164
    }
165
166
    /**
167
     * Checks whether the session has expired or not
168
     * @return bool
169
     */
170
    private function isValid(): bool
171
    {
172
        if (isset($_SESSION['OBSOLETE']) && !isset($_SESSION['EXPIRES'])) {
173
            return false;
174
        }
175
176
        if (isset($_SESSION['EXPIRES']) && $_SESSION['EXPIRES'] < time()) {
177
            return false;
178
        }
179
180
        return true;
181
    }
182
183
    /**
184
     *  Resets the session
185
     */
186
    public static function destroySession()
187
    {
188
        $id = session_id();
189
190
        if (!empty($id)) {
191
            $_SESSION = [];
192
            session_destroy();
193
            session_start();
194
        }
195
    }
196
197
    /**
198
     * @param string $key
199
     * @param mixed $val
200
     */
201
    public function set(string $key, $val): void
202
    {
203
        $_SESSION[$key] = $val;
204
    }
205
206
    /**
207
     * @param $key
208
     * @return null|mixed
209
     */
210
    public function get(string $key)
211
    {
212
        return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
213
    }
214
215
    /**
216
     * @param string $key
217
     * @return bool
218
     */
219
    public function has(string $key): bool
220
    {
221
        return isset($_SESSION[$key]);
222
    }
223
224
    /**
225
     * @param $key
226
     * @param $val
227
     */
228
    public function unset(string $key): void
229
    {
230
        unset($_SESSION[$key]);
231
    }
232
233
    /**
234
     * @param $key
235
     * @param $val
236
     * @deprecated use unset
237
     */
238
    public function destroy(string $key): void
239
    {
240
        unset($_SESSION[$key]);
241
    }
242
}
243