Passed
Push — master ( e0f947...82ed42 )
by Michael
17:30 queued 07:57
created

XoopsSessionHandler::regenerate_id()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 14
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 1
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
// --- START: New Domain Validation Logic ---
86
        $host = parse_url(XOOPS_URL, PHP_URL_HOST);
87
        if (!is_string($host)) {
0 ignored issues
show
introduced by
The condition is_string($host) is always true.
Loading history...
88
            $host = ''; // Fallback in case of invalid XOOPS_URL
89
        }
90
        $cookieDomain = XOOPS_COOKIE_DOMAIN;
91
        if (class_exists('\Xoops\RegDom\RegisteredDomain')) {
92
            if (!\Xoops\RegDom\RegisteredDomain::domainMatches($host, $cookieDomain)) {
93
                $cookieDomain = ''; // The corrected, safe domain
94
            }
95
        }
96
// --- END: New Domain Validation Logic ---
97
98
        if (PHP_VERSION_ID >= 70300) {
99
            $options = [
100
                'lifetime' => $lifetime,
101
                'path' => '/',
102
                'domain' => $cookieDomain,
103
                'secure' => $secure,
104
                'httponly' => true,
105
                'samesite' => 'Lax',
106
            ];
107
            session_set_cookie_params($options);
108
        } else {
109
            session_set_cookie_params($lifetime, '/', $cookieDomain, $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

109
            session_set_cookie_params(/** @scrutinizer ignore-type */ $lifetime, '/', $cookieDomain, $secure, true);
Loading history...
110
        }
111
    }
112
113
    /**
114
     * Open a session
115
     *
116
     * @param string $savePath
117
     * @param string $sessionName
118
     *
119
     * @return bool
120
     */
121
    public function open($savePath, $sessionName): bool
122
    {
123
        return true;
124
    }
125
126
    /**
127
     * Close a session
128
     *
129
     * @return bool
130
     */
131
    public function close(): bool
132
    {
133
        $this->gc_force();
134
        return true;
135
    }
136
137
    /**
138
     * Read a session from the database
139
     *
140
     * @param string $sessionId ID of the session
141
     *
142
     * @return string Session data (empty string if no data or failure)
143
     */
144
    public function read($sessionId): string
145
    {
146
        $ip = \Xmf\IPAddress::fromRequest();
147
        $sql = sprintf(
148
            'SELECT sess_data, sess_ip FROM %s WHERE sess_id = %s',
149
            $this->db->prefix('session'),
150
            $this->db->quote($sessionId)
151
        );
152
153
        $result = $this->db->queryF($sql);
154
        if ($this->db->isResultSet($result)) {
155
            if ([$sess_data, $sess_ip] = $this->db->fetchRow($result)) {
156
                if ($this->securityLevel > 1) {
157
                    if (false === $ip->sameSubnet(
158
                            $sess_ip,
159
                            $this->bitMasks[$this->securityLevel]['v4'],
160
                            $this->bitMasks[$this->securityLevel]['v6']
161
                        )) {
162
                        $sess_data = '';
163
                    }
164
                }
165
                return $sess_data;
166
            }
167
        }
168
        return '';
169
    }
170
171
    /**
172
     * Write a session to the database
173
     *
174
     * @param string $sessionId
175
     * @param string $data
176
     *
177
     * @return bool
178
     */
179
    public function write($sessionId, $data): bool
180
    {
181
        $myReturn = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $myReturn is dead and can be removed.
Loading history...
182
        $remoteAddress = \Xmf\IPAddress::fromRequest()->asReadable();
183
        $sessionId = $this->db->quote($sessionId);
184
        
185
        $sql= sprintf('INSERT INTO %s (sess_id, sess_updated, sess_ip, sess_data)
186
        VALUES (%s, %u, %s, %s)
187
        ON DUPLICATE KEY UPDATE
188
        sess_updated = %u, 
189
        sess_data = %s
190
        ',
191
              $this->db->prefix('session'),
192
              $sessionId,
193
              time(),
194
              $this->db->quote($remoteAddress),
195
              $this->db->quote($data),
196
              time(),
197
              $this->db->quote($data),
198
        );
199
        $myReturn = $this->db->queryF($sql);
200
        $this->update_cookie();
201
        return $myReturn;
202
    }
203
204
    /**
205
     * Destroy a session
206
     *
207
     * @param string $sessionId
208
     *
209
     * @return bool
210
     */
211
    public function destroy($sessionId): bool
212
    {
213
        $sql = sprintf(
214
            'DELETE FROM %s WHERE sess_id = %s',
215
            $this->db->prefix('session'),
216
            $this->db->quote($sessionId)
217
        );
218
        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...
219
            return false;
220
        }
221
        return true;
222
    }
223
224
    /**
225
     * Garbage Collector
226
     *
227
     * @param int $expire Time in seconds until a session expires
228
     * @return int|bool The number of deleted sessions on success, or false on failure
229
     */
230
    #[\ReturnTypeWillChange]
231
    public function gc($expire)
232
    {
233
        if (empty($expire)) {
234
            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...
235
        }
236
237
        $mintime = time() - (int)$expire;
238
        $sql = sprintf('DELETE FROM %s WHERE sess_updated < %u', $this->db->prefix('session'), $mintime);
239
240
        if ($this->db->queryF($sql)) {
241
            return $this->db->getAffectedRows();
242
        }
243
        return false;
244
    }
245
246
    /**
247
     * Force gc for situations where gc is registered but not executed
248
     **/
249
    public function gc_force()
250
    {
251
        if (mt_rand(1, 100) < 11) {
252
            $expire = @ini_get('session.gc_maxlifetime');
253
            $expire = ($expire > 0) ? $expire : 900;
254
            $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

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