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