XoopsSessionHandler   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 212
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 120
c 2
b 1
f 0
dl 0
loc 212
rs 9.68
wmc 34

10 Methods

Rating   Name   Duplication   Size   Complexity  
A gc() 0 17 3
A write() 0 23 1
B __construct() 0 35 7
A close() 0 4 1
A destroy() 0 8 1
A open() 0 3 1
A read() 0 27 5
A gc_force() 0 17 4
B update_cookie() 0 26 8
A regenerate_id() 0 10 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
 * XOOPS session handler (PHP 7.4 compatible)
24
 * @package             kernel
25
 *
26
 * @author              Kazumi Ono    <[email protected]>
27
 * @author              Taiwen Jiang <[email protected]>
28
 * @author              XOOPS Development Team
29
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
30
 */
31
32
33
class XoopsSessionHandler implements \SessionHandlerInterface
34
{
35
    /** @var XoopsDatabase */
36
    public $db;
37
38
    /** @var int */
39
    public $securityLevel = 3;
40
41
    protected $bitMasks = [
42
        2 => ['v4' => 16, 'v6' => 64],
43
        3 => ['v4' => 24, 'v6' => 56],
44
        4 => ['v4' => 32, 'v6' => 128],
45
    ];
46
47
    /** @var bool */
48
    public $enableRegenerateId = true;
49
50
    public function __construct(XoopsDatabase $db)
51
    {
52
        global $xoopsConfig;
53
        $this->db = $db;
54
55
        $lifetime = ($xoopsConfig['use_mysession'] && $xoopsConfig['session_name'] != '')
56
            ? $xoopsConfig['session_expire'] * 60
57
            : ini_get('session.cookie_lifetime');
58
59
        $secure = (XOOPS_PROT === 'https://');
60
61
        // Domain validation from your current code
62
        $host = parse_url(XOOPS_URL, PHP_URL_HOST);
63
        if (!is_string($host)) {
0 ignored issues
show
introduced by
The condition is_string($host) is always true.
Loading history...
64
            $host = '';
65
        }
66
        $cookieDomain = XOOPS_COOKIE_DOMAIN;
67
        if (class_exists('\Xoops\RegDom\RegisteredDomain')) {
68
            if (!\Xoops\RegDom\RegisteredDomain::domainMatches($host, $cookieDomain)) {
69
                $cookieDomain = '';
70
            }
71
        }
72
73
        if (PHP_VERSION_ID >= 70300) {
74
            $options = [
75
                'lifetime' => $lifetime,
76
                'path'     => '/',
77
                'domain'   => $cookieDomain,
78
                'secure'   => $secure,
79
                'httponly' => true,
80
                'samesite' => 'Lax',
81
            ];
82
            session_set_cookie_params($options);
83
        } else {
84
            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

84
            session_set_cookie_params(/** @scrutinizer ignore-type */ $lifetime, '/', $cookieDomain, $secure, true);
Loading history...
85
        }
86
    }
87
88
    // --- SessionHandlerInterface (untyped for 7.4) ---
89
90
    public function open($savePath, $sessionName)
91
    {
92
        return true;
93
    }
94
95
    public function close()
96
    {
97
        $this->gc_force();
98
        return true;
99
    }
100
101
    public function read($sessionId)
102
    {
103
        $ip = \Xmf\IPAddress::fromRequest();
104
        $sql = sprintf(
105
            'SELECT sess_data, sess_ip FROM %s WHERE sess_id = %s',
106
            $this->db->prefix('session'),
107
            $this->db->quote($sessionId)
108
        );
109
        $result = $this->db->queryF($sql);
0 ignored issues
show
Deprecated Code introduced by
The function XoopsDatabase::queryF() has been deprecated. ( Ignorable by Annotation )

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

109
        $result = /** @scrutinizer ignore-deprecated */ $this->db->queryF($sql);
Loading history...
110
        if (!$this->db->isResultSet($result)) {
111
            return false; // failure -> false for consistency with session80.php
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by SessionHandlerInterface::read() of string.

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...
112
        }
113
        $row = $this->db->fetchRow($result);
0 ignored issues
show
Bug introduced by
The method fetchRow() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

113
        /** @scrutinizer ignore-call */ 
114
        $row = $this->db->fetchRow($result);
Loading history...
114
        if ($row === false) {
115
            return ''; // not found -> empty string
116
        }
117
        [$sess_data, $sess_ip] = $row;
118
        if ($this->securityLevel > 1) {
119
            if (false === $ip->sameSubnet(
120
                    $sess_ip,
121
                    $this->bitMasks[$this->securityLevel]['v4'],
122
                    $this->bitMasks[$this->securityLevel]['v6']
123
                )) {
124
                return ''; // IP mismatch -> empty string
125
            }
126
        }
127
        return (string)$sess_data;
128
    }
129
130
    public function write($sessionId, $data)
131
    {
132
        $remoteAddress = \Xmf\IPAddress::fromRequest()->asReadable();
133
        $sid = $this->db->quote($sessionId);
134
        $now = time();
135
136
        $sql = sprintf(
137
            'INSERT INTO %s (sess_id, sess_updated, sess_ip, sess_data)
138
             VALUES (%s, %u, %s, %s)
139
             ON DUPLICATE KEY UPDATE
140
             sess_updated = %u, sess_data = %s',
141
            $this->db->prefix('session'),
142
            $sid,
143
            $now,
144
            $this->db->quote($remoteAddress),
145
            $this->db->quote($data),
146
            $now,
147
            $this->db->quote($data)
148
        );
149
150
        $ok = $this->db->exec($sql);
151
        $this->update_cookie();
152
        return (bool)$ok;
153
    }
154
155
    public function destroy($sessionId)
156
    {
157
        $sql = sprintf(
158
            'DELETE FROM %s WHERE sess_id = %s',
159
            $this->db->prefix('session'),
160
            $this->db->quote($sessionId)
161
        );
162
        return (bool)$this->db->exec($sql);
163
    }
164
165
    public function gc($max_lifetime)
166
    {
167
        if ($max_lifetime <= 0) {
168
            return 0; // return int for 7.4
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...
169
        }
170
171
        $mintime = time() - (int)$max_lifetime;
172
        $sql = sprintf(
173
            'DELETE FROM %s WHERE sess_updated < %u',
174
            $this->db->prefix('session'),
175
            $mintime
176
        );
177
178
        if ($this->db->exec($sql)) {
179
            return (int)$this->db->getAffectedRows();
0 ignored issues
show
Bug introduced by
The method getAffectedRows() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

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

179
            return (int)$this->db->/** @scrutinizer ignore-call */ getAffectedRows();
Loading history...
Bug Best Practice introduced by
The expression return (int)$this->db->getAffectedRows() 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...
180
        }
181
        return 0; // int on failure
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...
182
    }
183
184
    // --- Helpers from your current code ---
185
186
    public function gc_force(): void
187
    {
188
        // Use \random_int() instead of mt_rand() to get a cryptographically secure,
189
        // uniformly distributed random number for probabilistic garbage collection.
190
        // This preserves the approximate 10% chance of forcing gc() (1–100 < 11),
191
        // while acknowledging that the underlying RNG changed from mt_rand().
192
        // The try/catch ensures that a failure in \random_int() (e.g. missing CSPRNG)
193
        // does not break session handling; in that case, gc() is simply skipped here.
194
        try {
195
            if (\random_int(1, 100) < 11) {
196
                $expire = (int) @ini_get('session.gc_maxlifetime');
197
                if ($expire <= 0) {
198
                    $expire = 900;
199
                }
200
                $this->gc($expire);
201
            }
202
        } catch (\Throwable $e) {
203
            // ignore
204
        }
205
    }
206
207
    public function regenerate_id($delete_old_session = false)
208
    {
209
        $success = $this->enableRegenerateId
210
            ? session_regenerate_id((bool)$delete_old_session)
211
            : true;
212
213
        if ($success) {
214
            $this->update_cookie();
215
        }
216
        return $success;
217
    }
218
219
    public function update_cookie($sess_id = null, $expire = null)
220
    {
221
        if (PHP_VERSION_ID < 70300) {
222
            global $xoopsConfig;
223
            $session_name = session_name();
224
            $session_expire = null !== $expire
225
                ? (int)$expire
226
                : (
227
                ($xoopsConfig['use_mysession'] && $xoopsConfig['session_name'] != '')
228
                    ? $xoopsConfig['session_expire'] * 60
229
                    : ini_get('session.cookie_lifetime')
230
                );
231
            $session_id = empty($sess_id) ? session_id() : $sess_id;
232
            $cookieDomain = XOOPS_COOKIE_DOMAIN;
233
            if (2 > substr_count($cookieDomain, '.')) {
234
                $cookieDomain = '.' . $cookieDomain;
235
            }
236
237
            xoops_setcookie(
238
                $session_name,
239
                $session_id,
240
                $session_expire ? time() + $session_expire : 0,
241
                '/',
242
                $cookieDomain,
243
                (XOOPS_PROT === 'https://'),
244
                true
245
            );
246
        }
247
    }
248
}
249