XoopsSessionHandler   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 275
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 104
dl 0
loc 275
rs 9.92
c 1
b 0
f 1
wmc 31

10 Methods

Rating   Name   Duplication   Size   Complexity  
A open() 0 3 1
A __construct() 0 22 4
A close() 0 4 1
A read() 0 25 5
A gc_force() 0 6 3
A gc() 0 14 3
B update_cookie() 0 26 8
A write() 0 23 1
A destroy() 0 11 2
A regenerate_id() 0 14 3
1
<?php
2
/**
3
 * XOOPS session handler
4
 *
5
 * You may not change or alter any portion of this comment or credits
6
 * of supporting developers from this source code or any supporting source code
7
 * which is considered copyrighted (c) material of the original comment or credit authors.
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 *
12
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
13
 * @license             GNU GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html)
14
 * @package             kernel
15
 * @since               2.0.0
16
 * @author              Kazumi Ono (AKA onokazu) http://www.myweb.ne.jp/, http://jp.xoops.org/
17
 * @author              Taiwen Jiang <[email protected]>
18
 */
19
20
defined('XOOPS_ROOT_PATH') || exit('Restricted access');
21
22
/**
23
 * Handler for a session
24
 * @package             kernel
25
 *
26
 * @author              Kazumi Ono    <[email protected]>
27
 * @author              Taiwen Jiang <[email protected]>
28
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
29
 */
30
class XoopsSessionHandler implements SessionHandlerInterface
31
{
32
    /**
33
     * Database connection
34
     *
35
     * @var object
36
     * @access    private
37
     */
38
    public $db;
39
40
    /**
41
     * Security checking level
42
     *
43
     * Possible value:
44
     *    0 - no check;
45
     *    1 - check browser characteristics (HTTP_USER_AGENT/HTTP_ACCEPT_LANGUAGE), to be implemented in the future now;
46
     *    2 - check browser and IP A.B;
47
     *    3 - check browser and IP A.B.C, recommended;
48
     *    4 - check browser and IP A.B.C.D;
49
     *
50
     * @var int
51
     * @access    public
52
     */
53
    public $securityLevel = 3;
54
55
    protected $bitMasks = [
56
        2 => ['v4' => 16, 'v6' => 64],
57
        3 => ['v4' => 24, 'v6' => 56],
58
        4 => ['v4' => 32, 'v6' => 128],
59
    ];
60
61
    /**
62
     * Enable regenerate_id
63
     *
64
     * @var bool
65
     * @access    public
66
     */
67
    public $enableRegenerateId = true;
68
69
    /**
70
     * Constructor
71
     *
72
     * @param XoopsDatabase $db reference to the {@link XoopsDatabase} object
73
     *
74
     */
75
    public function __construct(XoopsDatabase $db)
76
    {
77
        global $xoopsConfig;
78
79
        $this->db = $db;
80
        // after php 7.3 we just let php handle the session cookie
81
        $lifetime = ($xoopsConfig['use_mysession'] && $xoopsConfig['session_name'] != '')
82
            ? $xoopsConfig['session_expire'] * 60
83
            : ini_get('session.cookie_lifetime');
84
        $secure = (XOOPS_PROT === 'https://');
85
        if (PHP_VERSION_ID >= 70300) {
86
            $options = [
87
                'lifetime' => $lifetime,
88
                'path'     => '/',
89
                'domain'   => XOOPS_COOKIE_DOMAIN,
90
                'secure'   => $secure,
91
                'httponly' => true,
92
                'samesite' => 'Lax',
93
            ];
94
            session_set_cookie_params($options);
95
        } else {
96
            session_set_cookie_params($lifetime, '/', XOOPS_COOKIE_DOMAIN, $secure, true);
0 ignored issues
show
Bug introduced by
It seems like $lifetime can also be of type string; however, parameter $lifetime_or_options of session_set_cookie_params() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

96
            session_set_cookie_params(/** @scrutinizer ignore-type */ $lifetime, '/', XOOPS_COOKIE_DOMAIN, $secure, true);
Loading history...
97
        }
98
    }
99
100
    /**
101
     * Open a session
102
     *
103
     * @param string $savePath
104
     * @param string $sessionName
105
     *
106
     * @return bool
107
     */
108
    public function open($savePath, $sessionName): bool
109
    {
110
        return true;
111
    }
112
113
    /**
114
     * Close a session
115
     *
116
     * @return bool
117
     */
118
    public function close(): bool
119
    {
120
        $this->gc_force();
121
        return true;
122
    }
123
124
    /**
125
     * Read a session from the database
126
     *
127
     * @param string $sessionId ID of the session
128
     *
129
     * @return string Session data (empty string if no data or failure)
130
     */
131
    public function read($sessionId): string
132
    {
133
        $ip = \Xmf\IPAddress::fromRequest();
134
        $sql = sprintf(
135
            'SELECT sess_data, sess_ip FROM %s WHERE sess_id = %s',
136
            $this->db->prefix('session'),
137
            $this->db->quoteString($sessionId)
138
        );
139
140
        $result = $this->db->queryF($sql);
141
        if ($this->db->isResultSet($result)) {
142
            if ([$sess_data, $sess_ip] = $this->db->fetchRow($result)) {
143
                if ($this->securityLevel > 1) {
144
                    if (false === $ip->sameSubnet(
145
                            $sess_ip,
146
                            $this->bitMasks[$this->securityLevel]['v4'],
147
                            $this->bitMasks[$this->securityLevel]['v6']
148
                        )) {
149
                        $sess_data = '';
150
                    }
151
                }
152
                return $sess_data;
153
            }
154
        }
155
        return '';
156
    }
157
158
    /**
159
     * Write a session to the database
160
     *
161
     * @param string $sessionId
162
     * @param string $data
163
     *
164
     * @return bool
165
     */
166
    public function write($sessionId, $data): bool
167
    {
168
        $myReturn = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $myReturn is dead and can be removed.
Loading history...
169
        $remoteAddress = \Xmf\IPAddress::fromRequest()->asReadable();
170
        $sessionId = $this->db->quoteString($sessionId);
171
        
172
        $sql= sprintf('INSERT INTO %s (sess_id, sess_updated, sess_ip, sess_data)
173
        VALUES (%s, %u, %s, %s)
174
        ON DUPLICATE KEY UPDATE
175
        sess_updated = %u, 
176
        sess_data = %s
177
        ',
178
              $this->db->prefix('session'),
179
              $sessionId,
180
              time(),
181
              $this->db->quote($remoteAddress),
182
              $this->db->quote($data),
183
              time(),
184
              $this->db->quote($data),
185
        );
186
        $myReturn = $this->db->queryF($sql);
187
        $this->update_cookie();
188
        return $myReturn;
189
    }
190
191
    /**
192
     * Destroy a session
193
     *
194
     * @param string $sessionId
195
     *
196
     * @return bool
197
     */
198
    public function destroy($sessionId): bool
199
    {
200
        $sql = sprintf(
201
            'DELETE FROM %s WHERE sess_id = %s',
202
            $this->db->prefix('session'),
203
            $this->db->quoteString($sessionId)
204
        );
205
        if (!$result = $this->db->queryF($sql)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
206
            return false;
207
        }
208
        return true;
209
    }
210
211
    /**
212
     * Garbage Collector
213
     *
214
     * @param int $expire Time in seconds until a session expires
215
     * @return int|bool The number of deleted sessions on success, or false on failure
216
     */
217
    #[\ReturnTypeWillChange]
218
    public function gc($expire)
219
    {
220
        if (empty($expire)) {
221
            return 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return 0 returns the type integer which is incompatible with the return type mandated by SessionHandlerInterface::gc() of boolean.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
222
        }
223
224
        $mintime = time() - (int)$expire;
225
        $sql = sprintf('DELETE FROM %s WHERE sess_updated < %u', $this->db->prefix('session'), $mintime);
226
227
        if ($this->db->queryF($sql)) {
228
            return $this->db->getAffectedRows();
229
        }
230
        return false;
231
    }
232
233
    /**
234
     * Force gc for situations where gc is registered but not executed
235
     **/
236
    public function gc_force()
237
    {
238
        if (mt_rand(1, 100) < 11) {
239
            $expire = @ini_get('session.gc_maxlifetime');
240
            $expire = ($expire > 0) ? $expire : 900;
241
            $this->gc($expire);
0 ignored issues
show
Bug introduced by
It seems like $expire can also be of type false and string; however, parameter $expire of XoopsSessionHandler::gc() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

241
            $this->gc(/** @scrutinizer ignore-type */ $expire);
Loading history...
242
        }
243
    }
244
245
    /**
246
     * Update the current session id with a newly generated one
247
     *
248
     * To be refactored
249
     *
250
     * @param  bool $delete_old_session
251
     * @return bool
252
     **/
253
    public function regenerate_id($delete_old_session = false)
254
    {
255
        if (!$this->enableRegenerateId) {
256
            $success = true;
257
        } else {
258
            $success = session_regenerate_id($delete_old_session);
259
        }
260
261
        // Force updating cookie for session cookie
262
        if ($success) {
263
            $this->update_cookie();
264
        }
265
266
        return $success;
267
    }
268
269
    /**
270
     * Update cookie status for current session
271
     *
272
     * To be refactored
273
     * FIXME: how about $xoopsConfig['use_ssl'] is enabled?
274
     *
275
     * @param  string $sess_id session ID
276
     * @param  int    $expire  Time in seconds until a session expires
277
     * @return bool
278
     **/
279
    public function update_cookie($sess_id = null, $expire = null)
280
    {
281
        if (PHP_VERSION_ID < 70300) {
282
            global $xoopsConfig;
283
            $session_name = session_name();
284
            $session_expire = null !== $expire
285
                ? (int)$expire
286
                : (
287
                    ($xoopsConfig['use_mysession'] && $xoopsConfig['session_name'] != '')
288
                    ? $xoopsConfig['session_expire'] * 60
289
                    : ini_get('session.cookie_lifetime')
290
                );
291
            $session_id = empty($sess_id) ? session_id() : $sess_id;
292
            $cookieDomain = XOOPS_COOKIE_DOMAIN;
293
            if (2 > substr_count($cookieDomain, '.')) {
294
                $cookieDomain  = '.' . $cookieDomain ;
295
            }
296
297
            xoops_setcookie(
298
                $session_name,
299
                $session_id,
300
                $session_expire ? time() + $session_expire : 0,
301
                '/',
302
                $cookieDomain,
303
                (XOOPS_PROT === 'https://'),
304
                true,
305
            );
306
        }
307
    }
308
}
309