Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Database based session handler.
19
 *
20
 * @package    core
21
 * @copyright  2013 Petr Skoda {@link http://skodak.org}
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core\session;
26
 
27
use SessionHandlerInterface;
28
 
29
/**
30
 * Database based session handler.
31
 *
32
 * @package    core
33
 * @copyright  2013 Petr Skoda {@link http://skodak.org}
34
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class database extends handler implements SessionHandlerInterface {
37
    /** @var \stdClass $record session record */
38
    protected $recordid = null;
39
 
40
    /** @var \moodle_database $database session database */
41
    protected $database = null;
42
 
43
    /** @var bool $failed session read/init failed, do not write back to DB */
44
    protected $failed = false;
45
 
46
    /** @var string $lasthash hash of the session data content */
47
    protected $lasthash = null;
48
 
49
    /** @var int $acquiretimeout how long to wait for session lock */
50
    protected $acquiretimeout = 120;
51
 
52
    /**
53
     * Create new instance of handler.
54
     */
55
    public function __construct() {
56
        global $DB, $CFG;
57
        // Note: we store the reference here because we need to modify database in shutdown handler.
58
        $this->database = $DB;
59
 
60
        if (!empty($CFG->session_database_acquire_lock_timeout)) {
61
            $this->acquiretimeout = (int)$CFG->session_database_acquire_lock_timeout;
62
        }
63
    }
64
 
65
    /**
66
     * Init session handler.
67
     */
68
    public function init() {
69
        if (!$this->database->session_lock_supported()) {
70
            throw new exception('sessionhandlerproblem', 'error', '', null, 'Database does not support session locking');
71
        }
72
 
73
        $result = session_set_save_handler($this);
74
        if (!$result) {
75
            throw new exception('dbsessionhandlerproblem', 'error');
76
        }
77
    }
78
 
79
    /**
80
     * Check the backend contains data for this session id.
81
     *
82
     * Note: this is intended to be called from manager::session_exists() only.
83
     *
84
     * @param string $sid
85
     * @return bool true if session found.
86
     */
87
    public function session_exists($sid) {
88
        // It was already checked in the calling code that the record in sessions table exists.
89
        return true;
90
    }
91
 
92
    /**
93
     * Kill all active sessions, the core sessions table is
94
     * purged afterwards.
95
     */
96
    public function kill_all_sessions() {
97
        // Nothing to do, the sessions table is cleared from core.
98
        return;
99
    }
100
 
101
    /**
102
     * Kill one session, the session record is removed afterwards.
103
     * @param string $sid
104
     */
105
    public function kill_session($sid) {
106
        // Nothing to do, the sessions table is purged afterwards.
107
        return;
108
    }
109
 
110
    /**
111
     * Open session handler.
112
     *
113
     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
114
     *
115
     * @param string $path
116
     * @param string $name
117
     * @return bool success
118
     */
119
    public function open(string $path, string $name): bool {
120
        // Note: we use the already open database.
121
        return true;
122
    }
123
 
124
    /**
125
     * Close session handler.
126
     *
127
     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
128
     *
129
     * @return bool success
130
     */
131
    public function close(): bool {
132
        if ($this->recordid) {
133
            try {
134
                $this->database->release_session_lock($this->recordid);
135
            } catch (\Exception $ex) {
136
                // Ignore any problems.
137
            }
138
        }
139
        $this->recordid = null;
140
        $this->lasthash = null;
141
        return true;
142
    }
143
 
144
    /**
145
     * Read session handler.
146
     *
147
     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
148
     *
149
     * @param string $sid
150
     * @return string|false
151
     */
152
    public function read(string $sid): string|false {
153
        try {
154
            if (!$record = $this->database->get_record('sessions', array('sid'=>$sid), 'id')) {
155
                // Let's cheat and skip locking if this is the first access,
156
                // do not create the record here, let the manager do it after session init.
157
                $this->failed = false;
158
                $this->recordid = null;
159
                $this->lasthash = sha1('');
160
                return '';
161
            }
162
            if ($this->recordid and $this->recordid != $record->id) {
163
                error_log('Second session read with different record id detected, cannot read session');
164
                $this->failed = true;
165
                $this->recordid = null;
166
                return '';
167
            }
168
            if (!$this->recordid) {
169
                // Lock session if exists and not already locked.
170
                if ($this->requires_write_lock()) {
171
                    $this->database->get_session_lock($record->id, $this->acquiretimeout);
172
                }
173
                $this->recordid = $record->id;
174
            }
175
        } catch (\dml_sessionwait_exception $ex) {
176
            // This is a fatal error, better inform users.
177
            // It should not happen very often - all pages that need long time to execute
178
            // should close session immediately after access control checks.
179
            error_log('Cannot obtain session lock for sid: '.$sid);
180
            $this->failed = true;
181
            throw $ex;
182
 
183
        } catch (\Exception $ex) {
184
            // Do not rethrow exceptions here, this should not happen.
185
            error_log('Unknown exception when starting database session : '.$sid.' - '.$ex->getMessage());
186
            $this->failed = true;
187
            $this->recordid = null;
188
            return '';
189
        }
190
 
191
        // Finally read the full session data because we know we have the lock now.
192
        if (!$record = $this->database->get_record('sessions', array('id'=>$record->id), 'id, sessdata')) {
193
            // Ignore - something else just deleted the session record.
194
            $this->failed = true;
195
            $this->recordid = null;
196
            return '';
197
        }
198
        $this->failed = false;
199
 
200
        if (is_null($record->sessdata)) {
201
            $data = '';
202
            $this->lasthash = sha1('');
203
        } else {
204
            $data = base64_decode($record->sessdata);
205
            $this->lasthash = sha1($record->sessdata);
206
        }
207
 
208
        return $data;
209
    }
210
 
211
    /**
212
     * Write session handler.
213
     *
214
     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
215
     *
216
     * NOTE: Do not write to output or throw any exceptions!
217
     *       Hopefully the next page is going to display nice error or it recovers...
218
     *
219
     * @param string $id
220
     * @param string $data
221
     * @return bool success
222
     */
223
    public function write(string $id, string $data): bool {
224
        if ($this->failed) {
225
            // Do not write anything back - we failed to start the session properly.
226
            return false;
227
        }
228
 
229
        // There might be some binary mess.
230
        $sessdata = base64_encode($data);
231
        $hash = sha1($sessdata);
232
 
233
        if ($hash === $this->lasthash) {
234
            return true;
235
        }
236
 
237
        try {
238
            if ($this->recordid) {
239
                $this->database->set_field('sessions', 'sessdata', $sessdata, ['id' => $this->recordid]);
240
            } else {
241
                // This happens in the first request when session record was just created in manager.
242
                $this->database->set_field('sessions', 'sessdata', $sessdata, ['sid' => $id]);
243
            }
244
        } catch (\Exception $ex) {
245
            // Do not rethrow exceptions here, this should not happen.
246
            // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative
247
            error_log(
248
                "Unknown exception when writing database session data : {$id} - " . $ex->getMessage(),
249
            );
250
        }
251
 
252
        return true;
253
    }
254
 
255
    /**
256
     * Destroy session handler.
257
     *
258
     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
259
     *
260
     * @param string $id
261
     * @return bool success
262
     */
263
    public function destroy(string $id): bool {
264
        if (!$session = $this->database->get_record('sessions', ['sid' => $id], 'id, sid')) {
265
            if ($id == session_id()) {
266
                $this->recordid = null;
267
                $this->lasthash = null;
268
            }
269
            return true;
270
        }
271
 
272
        if ($this->recordid && ($session->id == $this->recordid)) {
273
            try {
274
                $this->database->release_session_lock($this->recordid);
275
            } catch (\Exception $ex) {
276
                // Ignore problems.
277
            }
278
            $this->recordid = null;
279
            $this->lasthash = null;
280
        }
281
 
282
        $this->database->delete_records('sessions', ['id' => $session->id]);
283
 
284
        return true;
285
    }
286
 
287
    /**
288
     * GC session handler.
289
     *
290
     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
291
     *
292
     * @param int $max_lifetime moodle uses special timeout rules
293
     * @return bool success
294
     */
295
    // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameUnderscore
296
    public function gc(int $max_lifetime): int|false {
297
        // This should do something only if cron is not running properly...
298
        if (!$stalelifetime = ini_get('session.gc_maxlifetime')) {
299
            return false;
300
        }
301
        $params = ['purgebefore' => (time() - $stalelifetime)];
302
        $count = $this->database->count_records_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params);
303
        $this->database->delete_records_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params);
304
 
305
        return $count;
306
    }
307
}