Passed
Pull Request — master (#1589)
by Michael
09:15
created

XoopsSessionHandler::updateTimestamp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 6
c 1
b 1
f 0
nc 1
nop 2
dl 0
loc 9
rs 10
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 8.0+ with full types and lazy timestamp updates)
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
class XoopsSessionHandler implements
32
    \SessionHandlerInterface,
33
    \SessionUpdateTimestampHandlerInterface
34
{
35
    /** @var XoopsDatabase */
36
    public $db;
37
38
    /** @var int */
39
    public $securityLevel = 3;
40
41
    protected array $bitMasks = [
42
        2 => ['v4' => 16, 'v6' => 64],
43
        3 => ['v4' => 24, 'v6' => 56],
44
        4 => ['v4' => 32, 'v6' => 128],
45
    ];
46
47
    public bool $enableRegenerateId = true;
48
49
    public function __construct(XoopsDatabase $db)
50
    {
51
        global $xoopsConfig;
52
        $this->db = $db;
53
54
        $lifetime = ($xoopsConfig['use_mysession'] && $xoopsConfig['session_name'] != '')
55
            ? $xoopsConfig['session_expire'] * 60
56
            : ini_get('session.cookie_lifetime');
57
58
        $secure = (XOOPS_PROT === 'https://');
59
60
        $host = parse_url(XOOPS_URL, PHP_URL_HOST);
61
        if (!is_string($host)) {
0 ignored issues
show
introduced by
The condition is_string($host) is always true.
Loading history...
62
            $host = '';
63
        }
64
        $cookieDomain = XOOPS_COOKIE_DOMAIN;
65
        if (class_exists('\Xoops\RegDom\RegisteredDomain')) {
66
            if (!\Xoops\RegDom\RegisteredDomain::domainMatches($host, $cookieDomain)) {
67
                $cookieDomain = '';
68
            }
69
        }
70
71
        $options = [
72
            'lifetime' => $lifetime,
73
            'path'     => '/',
74
            'domain'   => $cookieDomain,
75
            'secure'   => $secure,
76
            'httponly' => true,
77
            'samesite' => 'Lax',
78
        ];
79
        session_set_cookie_params($options);
80
    }
81
82
    // --- SessionHandlerInterface (typed) ---
83
84
    public function open(string $savePath, string $sessionName): bool
85
    {
86
        return true;
87
    }
88
89
    public function close(): bool
90
    {
91
        $this->gc_force();
92
        return true;
93
    }
94
95
    public function read(string $sessionId): string|false
96
    {
97
        $ip = \Xmf\IPAddress::fromRequest();
98
        $sql = sprintf(
99
            'SELECT sess_data, sess_ip FROM %s WHERE sess_id = %s',
100
            $this->db->prefix('session'),
101
            $this->db->quote($sessionId)
102
        );
103
104
        $result = $this->db->queryF($sql);
105
        if (!$this->db->isResultSet($result)) {
106
            return false; // storage failure
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...
107
        }
108
109
        $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

109
        /** @scrutinizer ignore-call */ 
110
        $row = $this->db->fetchRow($result);
Loading history...
110
        if ($row === false) {
111
            return ''; // not found → empty string
112
        }
113
114
        [$sess_data, $sess_ip] = $row;
115
116
        if ($this->securityLevel > 1) {
117
            if (false === $ip->sameSubnet(
118
                    $sess_ip,
119
                    $this->bitMasks[$this->securityLevel]['v4'],
120
                    $this->bitMasks[$this->securityLevel]['v6']
121
                )) {
122
                return ''; // IP mismatch → treat as no data
123
            }
124
        }
125
126
        return $sess_data;
127
    }
128
129
    public function write(string $sessionId, string $data): bool
130
    {
131
        $remoteAddress = \Xmf\IPAddress::fromRequest()->asReadable();
132
        $sid = $this->db->quote($sessionId);
133
        $now = time();
134
135
        $sql = sprintf(
136
            'INSERT INTO %s (sess_id, sess_updated, sess_ip, sess_data)
137
             VALUES (%s, %u, %s, %s)
138
             ON DUPLICATE KEY UPDATE
139
             sess_updated = %u, sess_data = %s',
140
            $this->db->prefix('session'),
141
            $sid,
142
            $now,
143
            $this->db->quote($remoteAddress),
144
            $this->db->quote($data),
145
            $now,
146
            $this->db->quote($data)
147
        );
148
149
        $ok = $this->db->exec($sql);
150
        // update_cookie() only affects <7.3 in your code; on 8+ it is effectively a no-op
151
        $this->update_cookie();
152
        return (bool)$ok;
153
    }
154
155
    public function destroy(string $sessionId): bool
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(int $max_lifetime): int|false
166
    {
167
        if ($max_lifetime <= 0) {
168
            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...
169
        }
170
        $mintime = time() - $max_lifetime;
171
        $sql = sprintf(
172
            'DELETE FROM %s WHERE sess_updated < %u',
173
            $this->db->prefix('session'),
174
            $mintime
175
        );
176
        if ($this->db->exec($sql)) {
177
            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

177
            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...
178
        }
179
        return false;
180
    }
181
182
    // --- SessionUpdateTimestampHandlerInterface (8.0+) ---
183
184
    public function validateId(string $id): bool
185
    {
186
        $sql = sprintf(
187
            'SELECT 1 FROM %s WHERE sess_id = %s',
188
            $this->db->prefix('session'),
189
            $this->db->quote($id)
190
        );
191
        $res = $this->db->queryF($sql, 1, 0);
0 ignored issues
show
Unused Code introduced by
The call to XoopsDatabase::queryF() has too many arguments starting with 1. ( Ignorable by Annotation )

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

191
        /** @scrutinizer ignore-call */ 
192
        $res = $this->db->queryF($sql, 1, 0);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
192
        return $this->db->isResultSet($res) && $this->db->fetchRow($res) !== false;
193
    }
194
195
    public function updateTimestamp(string $id, string $data): bool
196
    {
197
        $sql = sprintf(
198
            'UPDATE %s SET sess_updated = %u WHERE sess_id = %s',
199
            $this->db->prefix('session'),
200
            time(),
201
            $this->db->quote($id)
202
        );
203
        return (bool)$this->db->exec($sql);
204
    }
205
206
    // --- Helpers (same behavior as your current code) ---
207
208
    public function gc_force(): void
209
    {
210
        try {
211
            if (\random_int(1, 100) < 11) {
212
                $expire = (int) @ini_get('session.gc_maxlifetime');
213
                if ($expire <= 0) {
214
                    $expire = 900;
215
                }
216
                $this->gc($expire);
217
            }
218
        } catch (\Throwable $e) {
219
            // ignore
220
        }
221
    }
222
223
    public function regenerate_id(bool $delete_old_session = false): bool
224
    {
225
        $success = $this->enableRegenerateId
226
            ? session_regenerate_id($delete_old_session)
227
            : true;
228
229
        if ($success) {
230
            $this->update_cookie();
231
        }
232
        return $success;
233
    }
234
235
    public function update_cookie($sess_id = null, $expire = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $sess_id is not used and could be removed. ( Ignorable by Annotation )

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

235
    public function update_cookie(/** @scrutinizer ignore-unused */ $sess_id = null, $expire = null): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $expire is not used and could be removed. ( Ignorable by Annotation )

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

235
    public function update_cookie($sess_id = null, /** @scrutinizer ignore-unused */ $expire = null): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
236
    {
237
        // no-op for 8.0+; retained for parity with 7.4 implementation
238
    }
239
}
240