Passed
Pull Request — master (#1578)
by Michael
10:43
created

XoopsSessionHandler::gc_force()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 0
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 = $_SERVER['HTTP_HOST'] ?? '';
87
        $cookieDomain = XOOPS_COOKIE_DOMAIN;
88
        if (class_exists('\Xoops\RegDom\RegisteredDomain')) {
89
            if (!\Xoops\RegDom\RegisteredDomain::domainMatches($host, $cookieDomain)) {
90
                $cookieDomain = ''; // The corrected, safe domain
91
            }
92
        }
93
// --- END: New Domain Validation Logic ---
94
95
        if (PHP_VERSION_ID >= 70300) {
96
            $options = [
97
                'lifetime' => $lifetime,
98
                'path' => '/',
99
                'domain' => $cookieDomain,
100
                'secure' => $secure,
101
                'httponly' => true,
102
                'samesite' => 'Lax',
103
            ];
104
            session_set_cookie_params($options);
105
        } else {
106
            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

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

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