MediaWiki master
PHPSessionHandler.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Session;
8
11use Psr\Log\LoggerInterface;
12use Psr\Log\NullLogger;
13use SessionHandlerInterface;
14use Wikimedia\PhpSessionSerializer;
15
23class PHPSessionHandler implements SessionHandlerInterface {
25 protected static $instance = null;
26
28 protected $enable = false;
29
31 protected $warn = true;
32
34 protected LoggerInterface $logger;
35
37 protected $sessionFieldCache = [];
38
40 $this->setEnableFlags(
42 );
43 if ( $manager instanceof SessionManager ) {
44 $manager->setupPHPSessionHandler( $this );
45 }
46 }
47
56 private function setEnableFlags( $PHPSessionHandling ) {
57 switch ( $PHPSessionHandling ) {
58 case 'enable':
59 $this->enable = true;
60 $this->warn = false;
61 break;
62
63 case 'warn':
64 $this->enable = true;
65 $this->warn = true;
66 break;
67
68 case 'disable':
69 $this->enable = false;
70 $this->warn = false;
71 break;
72 }
73 }
74
79 public static function isInstalled() {
80 return (bool)self::$instance;
81 }
82
87 public static function isEnabled() {
88 return self::$instance && self::$instance->enable;
89 }
90
94 public static function install( SessionManagerInterface $manager ) {
95 /* @var SessionManager $manager*/'@phan-var SessionManager $manager';
96 if ( self::$instance ) {
97 $manager->setupPHPSessionHandler( self::$instance );
98 return;
99 }
100
101 // @codeCoverageIgnoreStart
102 if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
103 throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
104 }
105 // @codeCoverageIgnoreEnd
106
107 self::$instance = new self( $manager );
108
109 // Close any auto-started session, before we replace it
110 session_write_close();
111
112 // Tell PHP not to mess with cookies itself
113 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
114 @ini_set( 'session.use_cookies', 0 );
115
116 // T124510: Disable automatic PHP session related cache headers.
117 // MediaWiki adds its own headers and the default PHP behavior may
118 // set headers such as 'Pragma: no-cache' that cause problems with
119 // some user agents.
120 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
121 @session_cache_limiter( '' );
122
123 // Also set a serialization handler
124 PhpSessionSerializer::setSerializeHandler();
125
126 // Register this as the save handler, and register an appropriate
127 // shutdown function.
128 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
129 @session_set_save_handler( self::$instance, true );
130 }
131
132 private function getSessionManager(): SessionManagerInterface {
133 // NOTE: PHPUnit tests also run in CLI mode, so we need to ignore them
134 // otherwise tests will break in CI.
135 if ( wfIsCLI() && !defined( 'MW_PHPUNIT_TEST' ) ) {
136 // T405450: Don't reuse a reference of the cached session manager
137 // object in command-line mode when spawning child processes. Always
138 // get a fresh instance. This is because during service reset, there
139 // could be references to services container that is disabled.
140 return MediaWikiServices::getInstance()->getSessionManager();
141 }
142
143 // @phan-suppress-next-line PhanTypeMismatchReturnNullable
144 return $this->manager;
145 }
146
152 public function setManager(
153 SessionManagerInterface $manager, LoggerInterface $logger
154 ) {
155 if ( $this->manager !== $manager ) {
156 // Close any existing session before we change stores
157 if ( $this->manager ) {
158 session_write_close();
159 }
160 $this->manager = $manager;
161 $this->logger = $logger;
162 PhpSessionSerializer::setLogger( $this->logger );
163 }
164 }
165
173 #[\ReturnTypeWillChange]
174 public function open( $save_path, $session_name ) {
175 if ( self::$instance !== $this ) {
176 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
177 }
178 if ( !$this->enable ) {
179 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
180 }
181 return true;
182 }
183
189 #[\ReturnTypeWillChange]
190 public function close() {
191 if ( self::$instance !== $this ) {
192 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
193 }
194 $this->sessionFieldCache = [];
195 return true;
196 }
197
204 #[\ReturnTypeWillChange]
205 public function read( $id ) {
206 if ( self::$instance !== $this ) {
207 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
208 }
209 if ( !$this->enable ) {
210 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
211 }
212
213 $session = $this->getSessionManager()->getSessionById( $id, false );
214
215 if ( !$session ) {
216 return '';
217 }
218 $session->persist();
219
220 $data = iterator_to_array( $session );
221 $this->sessionFieldCache[$id] = $data;
222 return (string)PhpSessionSerializer::encode( $data );
223 }
224
228 private function valueContainsAnyObject( mixed $value ): bool {
229 if ( is_object( $value ) ) {
230 return true;
231 }
232 if ( is_array( $value ) ) {
233 $result = false;
234 array_walk_recursive( $value, static function ( $val ) use ( &$result ) {
235 if ( is_object( $val ) ) {
236 $result = true;
237 }
238 } );
239 return $result;
240 }
241 return false;
242 }
243
253 #[\ReturnTypeWillChange]
254 public function write( $id, $dataStr ) {
255 if ( self::$instance !== $this ) {
256 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
257 }
258 if ( !$this->enable ) {
259 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
260 }
261
262 $session = $this->getSessionManager()->getSessionById( $id, true );
263 if ( !$session ) {
264 // This can happen under normal circumstances, if the session exists but is
265 // invalid. Let's emit a log warning instead of a PHP warning.
266 $this->logger->warning(
267 __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
268 [
269 'session' => $id,
270 ] );
271 return true;
272 }
273
274 // First, decode the string PHP handed us
275 $data = PhpSessionSerializer::decode( $dataStr );
276 if ( $data === null ) {
277 // @codeCoverageIgnoreStart
278 return false;
279 // @codeCoverageIgnoreEnd
280 }
281
282 // Now merge the data into the Session object.
283 $changed = [];
284 $cache = $this->sessionFieldCache[$id] ?? [];
285 foreach ( $data as $key => $value ) {
286 if ( !array_key_exists( $key, $cache ) ) {
287 if ( $session->exists( $key ) ) {
288 // New in both, so ignore and log
289 $this->logger->warning(
290 __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
291 );
292 } else {
293 // New in $_SESSION, keep it
294 $session->set( $key, $value );
295 $changed[] = $key;
296 }
297 } elseif ( $cache[$key] === $value ) {
298 // Unchanged in $_SESSION, so ignore it
299 } elseif (
300 $this->valueContainsAnyObject( $cache[$key] ) &&
301 $this->valueContainsAnyObject( $value ) &&
302 PhpSessionSerializer::encode( [ $key => $cache[$key] ] ) ===
303 PhpSessionSerializer::encode( [ $key => $value ] )
304 ) {
305 // Also unchanged in $_SESSION. The values go through a serialize-and-deserialize
306 // operation before they get here, so if anyone stored any objects in session data,
307 // they will not compare as equal with `===`. Compare their serialized representation
308 // in that case to avoid unnecessary session writes and spurious warnings. (T402602)
309 } elseif ( !$session->exists( $key ) ) {
310 // Deleted in Session, keep but log
311 $this->logger->warning(
312 __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
313 );
314 $session->set( $key, $value );
315 $changed[] = $key;
316 } elseif ( $cache[$key] === $session->get( $key ) ) {
317 // Unchanged in Session, so keep it
318 $session->set( $key, $value );
319 $changed[] = $key;
320 } else {
321 // Changed in both, so ignore and log
322 $this->logger->warning(
323 __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
324 );
325 }
326 }
327 // Anything deleted in $_SESSION and unchanged in Session should be deleted too
328 // (but not if $_SESSION can't represent it at all)
329 PhpSessionSerializer::setLogger( new NullLogger() );
330 foreach ( $cache as $key => $value ) {
331 if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
332 PhpSessionSerializer::encode( [ $key => true ] )
333 ) {
334 if ( $value === $session->get( $key ) ) {
335 // Unchanged in Session, delete it
336 $session->remove( $key );
337 $changed[] = $key;
338 } else {
339 // Changed in Session, ignore deletion and log
340 $this->logger->warning(
341 __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
342 );
343 }
344 }
345 }
346 PhpSessionSerializer::setLogger( $this->logger );
347
348 // Save and update cache if anything changed
349 if ( $changed ) {
350 if ( $this->warn ) {
351 wfDeprecated( '$_SESSION', '1.27' );
352 foreach ( $changed as $key ) {
353 $this->logger->warning( "Something wrote to \$_SESSION['$key']!" );
354 }
355 }
356
357 $session->save();
358 $this->sessionFieldCache[$id] = iterator_to_array( $session );
359 }
360
361 $session->persist();
362
363 return true;
364 }
365
372 #[\ReturnTypeWillChange]
373 public function destroy( $id ) {
374 if ( self::$instance !== $this ) {
375 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
376 }
377 if ( !$this->enable ) {
378 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
379 }
380 $session = $this->getSessionManager()->getSessionById( $id, false );
381 if ( $session ) {
382 $session->clear();
383 }
384 return true;
385 }
386
394 #[\ReturnTypeWillChange]
395 public function gc( $maxlifetime ) {
396 if ( self::$instance !== $this ) {
397 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
398 }
399 return true;
400 }
401}
wfIsCLI()
Check if we are running from the commandline.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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.
static install(SessionManagerInterface $manager)
Install a session handler for the current web request.
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.
setManager(SessionManagerInterface $manager, LoggerInterface $logger)
close()
Close the session (handler)
array $sessionFieldCache
Track original session fields for later modification check.
open( $save_path, $session_name)
Initialize the session (handler)
bool $enable
Whether PHP session handling is enabled.
__construct(SessionManagerInterface $manager)
This serves as the entry point to the MediaWiki session handling system.
MediaWiki\Session entry point interface.