xref: /webtrees/app/SessionDatabaseHandler.php (revision 1542a3c1c3f069aa1fa975b9754a150971b42c6f)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees;
21
22use Psr\Http\Message\ServerRequestInterface;
23use SessionHandlerInterface;
24
25use function date;
26use function time;
27
28/**
29 * Session handling - stores sessions in the database.
30 */
31class SessionDatabaseHandler implements SessionHandlerInterface
32{
33    private ServerRequestInterface $request;
34
35    private ?object $row = null;
36
37    /**
38     * @param ServerRequestInterface $request
39     */
40    public function __construct(ServerRequestInterface $request)
41    {
42        $this->request = $request;
43    }
44
45    /**
46     * @param string $path
47     * @param string $name
48     *
49     * @return bool
50     */
51    public function open(string $path, string $name): bool
52    {
53        return true;
54    }
55
56    /**
57     * @return bool
58     */
59    public function close(): bool
60    {
61        return true;
62    }
63
64    /**
65     * @param string $id
66     *
67     * @return string
68     */
69    public function read(string $id): string
70    {
71        $this->row = DB::table('session')
72            ->where('session_id', '=', $id)
73            ->first();
74
75        return $this->row->session_data ?? '';
76    }
77
78    /**
79     * @param string $id
80     * @param string $data
81     *
82     * @return bool
83     */
84    public function write(string $id, string $data): bool
85    {
86        $ip_address = Validator::attributes($this->request)->string('client-ip');
87        $user_id    = (int) Auth::id();
88        $now        = Registry::timestampFactory()->now();
89
90        if ($this->row === null) {
91            DB::table('session')->insert([
92                'session_id'   => $id,
93                'session_time' => $now->toDateTimeString(),
94                'user_id'      => $user_id,
95                'ip_address'   => $ip_address,
96                'session_data' => $data,
97            ]);
98        } else {
99            $updates = [];
100
101            // The user ID can change if we masquerade as another user.
102            if ((int) $this->row->user_id !== $user_id) {
103                $updates['user_id'] = $user_id;
104            }
105
106            if ($this->row->ip_address !== $ip_address) {
107                $updates['ip_address'] = $ip_address;
108            }
109
110            if ($this->row->session_data !== $data) {
111                $updates['session_data'] = $data;
112            }
113
114            // Only update session once a minute to reduce contention on the session table.
115            if ($now->subtractMinutes(1)->timestamp() > Registry::timestampFactory()->fromString($this->row->session_time)->timestamp()) {
116                $updates['session_time'] =  $now->toDateTimeString();
117            }
118
119            if ($updates !== []) {
120                DB::table('session')
121                    ->where('session_id', '=', $id)
122                    ->update($updates);
123            }
124        }
125
126        return true;
127    }
128
129    /**
130     * @param string $id
131     *
132     * @return bool
133     */
134    public function destroy(string $id): bool
135    {
136        DB::table('session')
137            ->where('session_id', '=', $id)
138            ->delete();
139
140        return true;
141    }
142
143    /**
144     * @param int $max_lifetime
145     *
146     * @return int
147     */
148    public function gc(int $max_lifetime): int
149    {
150        return DB::table('session')
151            ->where('session_time', '<', date('Y-m-d H:i:s', time() - $max_lifetime))
152            ->delete();
153    }
154}
155