MediaWiki REL1_39
PHPSessionHandler.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
26use BagOStuff;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use Wikimedia\AtEase\AtEase;
32
38class PHPSessionHandler implements \SessionHandlerInterface {
40 protected static $instance = null;
41
43 protected $enable = false;
44
46 protected $warn = true;
47
49 protected $manager;
50
52 protected $store;
53
55 protected $logger;
56
58 protected $sessionFieldCache = [];
59
60 protected function __construct( SessionManager $manager ) {
61 $this->setEnableFlags(
63 );
64 $manager->setupPHPSessionHandler( $this );
65 }
66
75 private function setEnableFlags( $PHPSessionHandling ) {
76 switch ( $PHPSessionHandling ) {
77 case 'enable':
78 $this->enable = true;
79 $this->warn = false;
80 break;
81
82 case 'warn':
83 $this->enable = true;
84 $this->warn = true;
85 break;
86
87 case 'disable':
88 $this->enable = false;
89 $this->warn = false;
90 break;
91 }
92 }
93
98 public static function isInstalled() {
99 return (bool)self::$instance;
100 }
101
106 public static function isEnabled() {
107 return self::$instance && self::$instance->enable;
108 }
109
114 public static function install( SessionManager $manager ) {
115 if ( self::$instance ) {
116 $manager->setupPHPSessionHandler( self::$instance );
117 return;
118 }
119
120 // @codeCoverageIgnoreStart
121 if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
122 throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
123 }
124 // @codeCoverageIgnoreEnd
125
126 self::$instance = new self( $manager );
127
128 // Close any auto-started session, before we replace it
129 session_write_close();
130
131 try {
132 AtEase::suppressWarnings();
133
134 // Tell PHP not to mess with cookies itself
135 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal Scalar okay with php8.1
136 ini_set( 'session.use_cookies', 0 );
137
138 // T124510: Disable automatic PHP session related cache headers.
139 // MediaWiki adds it's own headers and the default PHP behavior may
140 // set headers such as 'Pragma: no-cache' that cause problems with
141 // some user agents.
142 session_cache_limiter( '' );
143
144 // Also set a serialization handler
145 \Wikimedia\PhpSessionSerializer::setSerializeHandler();
146
147 // Register this as the save handler, and register an appropriate
148 // shutdown function.
149 session_set_save_handler( self::$instance, true );
150 } finally {
151 AtEase::restoreWarnings();
152 }
153 }
154
162 public function setManager(
164 ) {
165 if ( $this->manager !== $manager ) {
166 // Close any existing session before we change stores
167 if ( $this->manager ) {
168 session_write_close();
169 }
170 $this->manager = $manager;
171 $this->store = $store;
172 $this->logger = $logger;
173 \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
174 }
175 }
176
184 #[\ReturnTypeWillChange]
185 public function open( $save_path, $session_name ) {
186 if ( self::$instance !== $this ) {
187 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
188 }
189 if ( !$this->enable ) {
190 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
191 }
192 return true;
193 }
194
200 #[\ReturnTypeWillChange]
201 public function close() {
202 if ( self::$instance !== $this ) {
203 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
204 }
205 $this->sessionFieldCache = [];
206 return true;
207 }
208
215 #[\ReturnTypeWillChange]
216 public function read( $id ) {
217 if ( self::$instance !== $this ) {
218 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
219 }
220 if ( !$this->enable ) {
221 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
222 }
223
224 $session = $this->manager->getSessionById( $id, false );
225 if ( !$session ) {
226 return '';
227 }
228 $session->persist();
229
230 $data = iterator_to_array( $session );
231 $this->sessionFieldCache[$id] = $data;
232 return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
233 }
234
244 #[\ReturnTypeWillChange]
245 public function write( $id, $dataStr ) {
246 if ( self::$instance !== $this ) {
247 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
248 }
249 if ( !$this->enable ) {
250 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
251 }
252
253 $session = $this->manager->getSessionById( $id, true );
254 if ( !$session ) {
255 // This can happen under normal circumstances, if the session exists but is
256 // invalid. Let's emit a log warning instead of a PHP warning.
257 $this->logger->warning(
258 __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
259 [
260 'session' => $id,
261 ] );
262 return true;
263 }
264
265 // First, decode the string PHP handed us
266 $data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
267 if ( $data === null ) {
268 // @codeCoverageIgnoreStart
269 return false;
270 // @codeCoverageIgnoreEnd
271 }
272
273 // Now merge the data into the Session object.
274 $changed = false;
275 $cache = $this->sessionFieldCache[$id] ?? [];
276 foreach ( $data as $key => $value ) {
277 if ( !array_key_exists( $key, $cache ) ) {
278 if ( $session->exists( $key ) ) {
279 // New in both, so ignore and log
280 $this->logger->warning(
281 __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
282 );
283 } else {
284 // New in $_SESSION, keep it
285 $session->set( $key, $value );
286 $changed = true;
287 }
288 } elseif ( $cache[$key] === $value ) {
289 // Unchanged in $_SESSION, so ignore it
290 } elseif ( !$session->exists( $key ) ) {
291 // Deleted in Session, keep but log
292 $this->logger->warning(
293 __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
294 );
295 $session->set( $key, $value );
296 $changed = true;
297 } elseif ( $cache[$key] === $session->get( $key ) ) {
298 // Unchanged in Session, so keep it
299 $session->set( $key, $value );
300 $changed = true;
301 } else {
302 // Changed in both, so ignore and log
303 $this->logger->warning(
304 __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
305 );
306 }
307 }
308 // Anything deleted in $_SESSION and unchanged in Session should be deleted too
309 // (but not if $_SESSION can't represent it at all)
310 \Wikimedia\PhpSessionSerializer::setLogger( new NullLogger() );
311 foreach ( $cache as $key => $value ) {
312 if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
313 \Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
314 ) {
315 if ( $value === $session->get( $key ) ) {
316 // Unchanged in Session, delete it
317 $session->remove( $key );
318 $changed = true;
319 } else {
320 // Changed in Session, ignore deletion and log
321 $this->logger->warning(
322 __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
323 );
324 }
325 }
326 }
327 \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
328
329 // Save and update cache if anything changed
330 if ( $changed ) {
331 if ( $this->warn ) {
332 wfDeprecated( '$_SESSION', '1.27' );
333 $this->logger->warning( 'Something wrote to $_SESSION!' );
334 }
335
336 $session->save();
337 $this->sessionFieldCache[$id] = iterator_to_array( $session );
338 }
339
340 $session->persist();
341
342 return true;
343 }
344
351 #[\ReturnTypeWillChange]
352 public function destroy( $id ) {
353 if ( self::$instance !== $this ) {
354 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
355 }
356 if ( !$this->enable ) {
357 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
358 }
359 $session = $this->manager->getSessionById( $id, false );
360 if ( $session ) {
361 $session->clear();
362 }
363 return true;
364 }
365
373 #[\ReturnTypeWillChange]
374 public function gc( $maxlifetime ) {
375 if ( self::$instance !== $this ) {
376 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
377 }
378 $before = date( 'YmdHis', time() );
379 $this->store->deleteObjectsExpiringBefore( $before );
380 return true;
381 }
382}
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
A class containing constants representing the names of configuration variables.
const PHPSessionHandling
Name constant for the PHPSessionHandling setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Adapter for PHP's session handling.
gc( $maxlifetime)
Execute garbage collection.
write( $id, $dataStr)
Write session data.
static isInstalled()
Test whether the handler is installed.
static isEnabled()
Test whether the handler is installed and enabled.
SessionManagerInterface null $manager
close()
Close the session (handler)
array $sessionFieldCache
Track original session fields for later modification check.
open( $save_path, $session_name)
Initialize the session (handler)
static install(SessionManager $manager)
Install a session handler for the current web request.
bool $enable
Whether PHP session handling is enabled.
setManager(SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
This serves as the entry point to the MediaWiki session handling system.
This exists to make IDEs happy, so they don't see the internal-but-required-to-be-public methods on S...
$cache
Definition mcc.php:33
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...